diff --git a/.gitignore b/.gitignore index d10591ab..8076f8e3 100644 --- a/.gitignore +++ b/.gitignore @@ -21,7 +21,6 @@ dist/ js/src/inline.js coverage/ .nyc_output/ -py/escher/package.json # site files 1-0-0 @@ -32,3 +31,5 @@ package-lock.json node_modules/ .yarnclean .mypy_cache/ +/TODO.txt +/py/escher/package.json diff --git a/README.md b/README.md index b0145a45..3ed7c70f 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,7 @@ [![Travis](https://img.shields.io/travis/zakandrewking/escher/master.svg)](https://travis-ci.org/zakandrewking/escher) [![Coverage Status](https://img.shields.io/coveralls/zakandrewking/escher/master.svg)](https://coveralls.io/github/zakandrewking/escher?branch=master) -Escher -====== +# Escher Escher is a web-based tool to build, view, share, and embed metabolic maps. The easiest way to use Escher is to browse or build maps on the @@ -33,7 +32,116 @@ building, sharing, and embedding data-rich visualizations of biological pathways*, PLOS Computational Biology 11(8): e1004321. doi:[10.1371/journal.pcbi.1004321](http://dx.doi.org/10.1371/journal.pcbi.1004321) -Built at SBRG -============= +Escher was developed at [SBRG](http://systemsbiology.ucsd.edu/). Funding was +provided by [The National Science Foundation Graduate Research Fellowship](https://www.nsfgrfp.org) +under Grant no. DGE-1144086, The European Commission as part of a Marie Curie +International Outgoing Fellowship within the EU 7th Framework Program for +Research and Technological Development ([EU project AMBiCon, 332020](http://ec.europa.eu/research/mariecurieactions/node_en)), +and [The Novo Nordisk Foundation](http://novonordiskfonden.dk/) +through [The Center for Biosustainability](https://www.biosustain.dtu.dk/) +at the Technical University of Denmark (NNF10CC1016517) -[![SBRG](https://raw.githubusercontent.com/zakandrewking/escher/master/py/escher/static/img/sbrg-logo.png)](http://systemsbiology.ucsd.edu/) +# Building and testing Escher + +## JavaScript + +First, install dependencies with [npm](https://www.npmjs.com) (or you can use +[yarn](https://yarnpkg.com)): + +``` +npm install +``` + +Escher uses webpack to manage the build process. To run typical build steps, just run: + +``` +npm run build +``` + +You can run a development server with: + +``` +npm run start +# or for live updates when the source code changes: +npm run watch +``` + +To test the JavaScript files, run: + +``` +npm run test +``` + +## Python + +Escher has a Python package for generating Escher visualizations from within a +Python data anlaysis session. To learn more about using the features of the +Python package, check out the documentation: + +https://escher.readthedocs.io/en/latest/escher-python.html + +For development of the Python package, first build the JavaScript package and +copy it over to the `py` directory with these commands in the Escher root: + +``` +npm install +npm run build +npm run copy +``` + +Then in the `py` directory, install the Python package: + +``` +cd py +pip install -e . # installs escher in develop mode and dependencies +``` + +For Python testing, run this in the `py` directory: + +``` +cd py +pytest +``` + +## Jupyter extensions + +To develop the Jupyter notebook and Jupyter Lab extensions, you will need +install them with symlinks (the typical installation is describe in the +[docs](https://escher.readthedocs.io/en/latest/escher-python.html)). + +First, install the Python package for development as described above. + +For the Jupyter notebooks, run: + +``` +cd py +jupyter nbextension install --py --symlink --sys-prefix escher +jupyter nbextension enable --py --sys-prefix escher +``` + +When you make changes, you will need to `yarn copy` and refresh notebook browser +tab. + +For Jupyter Lab, run (in the root directory): + +``` +yarn watch # keep this running as a separate process +jupyter labextension install @jupyter-widgets/jupyterlab-manager +jupyter labextension link +jupyter lab --watch +``` + +If you don't see changes when you edit the code, try refreshing or restarting +`jupyter lab --watch`. + +## Docs + +Build and run the docs:: + +``` +cd docs +make html +cd _build/html +python -m SimpleHTTPServer # python 2 +python -m http.server # python 3 +``` diff --git a/docs/contribute_maps.rst b/docs/contribute_maps.rst index 6a74dd1c..4c5beec2 100644 --- a/docs/contribute_maps.rst +++ b/docs/contribute_maps.rst @@ -34,28 +34,6 @@ Once you have a COBRA model, you can follow these steps: iMM904.Amino acid biosynthesis.json -5. (Optional) Once you have a set of subsystem maps, you can set up a local - Escher server so that subsystem maps appear in the "quick jump" menu in the - bottom right corner of the screen (as seen here_ for iJO1366). To set this - up, you will need to start a local server as describe in - :ref:`local-server`. Next, find your local cache directory by running this - command in a terminal:: - - python -c "import escher; print(escher.get_cache_dir(name='maps'))" - - This will print the location of the local maps cache. Add your new subsystem - maps to cache folder. Now, when you run the server (described in - :ref:`local-server`), you should see that quick jump menu appear. - - NOTE: The cache directory is organized into folders for organisms. You can - use these folder for filtering by organism on the local launch page, or you - can place the maps in the top directory. - - NOTE 2: A similar approach can be used to access your models from the local - launch page. Place maps in the folder indicated by:: - - python -c "import escher; print(escher.get_cache_dir(name='models'))" - Building from an existing map for a similar organism ---------------------------------------------------- diff --git a/docs/development.rst b/docs/development.rst index 44cd7774..a56ff6e8 100644 --- a/docs/development.rst +++ b/docs/development.rst @@ -1,9 +1,15 @@ Developing with Escher ---------------------- -If you are interested in developing Escher or just want to try out the source -code, this is the place to start. You might also want to check out the the -`Gitter chat room`_ and the `Development Roadmap`_. +If you are interested in developing software using Escher as a dependency, this +is the place to start. If want contribute to development of Escher, then check +out the GitHub repository (instruction for building and testing the project are +in the README): + +https://github.com/zakandrewking/escher + +You might also want to check out the the `Gitter chat room`_ and the +`Development Roadmap`_. Using the static JavaScript and CSS files ========================================= @@ -49,41 +55,6 @@ If you are using JavaScript ES6 import syntax, the default export is Builder:: import * as escher from 'escher' console.log(Builder, escher.Builder, escher.libs.preact) -Building and testing Escher -=========================== - -First, install dependencies with npm:: - - npm install - -Escher uses grunt to manage the build process. To run typical build steps, just run:: - - npm run compile - -To test the JavaScript files, run:: - - npm run test - -For Python testing, run this in the ``py`` directory:: - - python setup.py test - -Build the static website:: - - python setup.py build_gh - -Clear static website files:: - - python setup.py clean - -Build and run the docs:: - - cd docs - make html - cd _build/html - python -m SimpleHTTPServer # python 2 - python -m http.server # python 3 - Generating and reading Escher and COBRA files ============================================= diff --git a/docs/escher-python.rst b/docs/escher-python.rst index ecb594c2..1a05deda 100644 --- a/docs/escher-python.rst +++ b/docs/escher-python.rst @@ -16,6 +16,15 @@ directly:: python setup.py install +To install the Jupyter notebook extension:: + + jupyter nbextension enable --py --sys-prefix escher # can be skipped for notebook 5.3 and above + +To install the Jupyter Lab extension:: + + jupyter labextension install @jupyter-widgets/jupyterlab-manager + jupyter labextension install escher + Dependencies should install automatically, but they are: - `Jinja2`_ @@ -58,20 +67,6 @@ Here are example notebooks to get started with: - `JavaScript development and offline maps`_ - `Generate JSON models in COBRApy`_ -.. _`local-server`: - -Running the local server -======================== - -You can run your own local server if you want to use Escher offline or explore -your own maps with the homepage browser. To get started, install the Python -package and run from any directory by calling:: - - python -m escher.server - -This starts a server at http://localhost:7778. You can also choose another port:: - - python -m escher.server --port=8005 .. _`source files`: https://github.com/zakandrewking/escher/releases diff --git a/docs/getting_started.rst b/docs/getting_started.rst index 986f8daf..cf811a5b 100644 --- a/docs/getting_started.rst +++ b/docs/getting_started.rst @@ -7,39 +7,6 @@ Introduction **Escher** is here to help you visualize pathway maps. But, if you have never heard of a pathway map, you might appreciate a quick introduction. -What are pathway maps? -^^^^^^^^^^^^^^^^^^^^^^ - -To understand pathway maps, it is useful to think about the general organization -of a cell. At the smallest level, molecules in a cell are arranged in -three-dimensional structures, and these structures determine many of the -functions that take place in a cell. For example, the 3D structure of an enzyme -determines the biochemical reactions that it can catalyze. These structures can -be visualized in 3D using tools like `Jmol`_ (as in this `example structure`_). - -The DNA sequence is a second fundamental level of biological organization. DNA -sequences are the blueprints for all the machinery of the cell, and they can be -visualized as a one-dimensional series of bases (ATCG) using tools like the -`UCSC genome browser`_. - -To use a football analogy, the 3D molecular structures are akin to the players -on the field, and the information in the DNA sequence is like the playbook on -the sidelines. But, football would not be very interesting if the players never -took to the field and executed those plays. So, we are missing this level of -detail: *the execution of biological plans by the molecular players*. - -What we are missing is the biochemical reaction network. Proteins in the cell -catalyze the conversion of substrate molecules into product molecules, and these -*reactions* are responsible for generating energy, constructing cellular machinery -and structures, detecting molecules in the environment, signaling, and -more. Biochemical reactions can be grouped into pathways when they work in -concert to carry out a function. (If a reaction is a football play, then the -pathway is a `drive`_). And Escher can be used to visualize these reactions and -pathways. Together, we call these visualizations **pathway maps**. - -Escher to the rescue -^^^^^^^^^^^^^^^^^^^^ - Many Escher maps represent *metabolic* pathways, and Escher was developed at the `Systems Biology Research Group`_ where we have been building genome-scale models of metabolism over the past fifteen years. However, Escher is not limited to diff --git a/docs/javascript_api.rst b/docs/javascript_api.rst index 8c517ea0..8c5d706c 100644 --- a/docs/javascript_api.rst +++ b/docs/javascript_api.rst @@ -42,11 +42,10 @@ JavaScript API .. js:attribute:: options.use_3d_transform - (Default: Chooses a good option by testing the browser) If true, then - use CSS3 3D transforms to speed up panning and zooming. This feature - will only work on browsers that `support the 3D transforms`_. It works - best in the latest versions of Chrome, Firefox and Internet - Explorer. Safari works better with this turned off. + (Default: false) If true, then use CSS3 3D transforms to speed up + panning and zooming. This feature will only work on browsers that + `support the 3D transforms`_. In general, this is no longer necessary + for modern browsers and hardware with small to medium sized maps. .. js:attribute:: options.enable_editing @@ -292,30 +291,35 @@ JavaScript API .. js:attribute:: options.tooltip_component - (Default: ``escher.Tooltip.DefaultTooltip``) A function or `tinier`_ - component to show when hoving over reactions, metabolites, and - genes. If a function is passed, the function will be called with a - single object as an argument with two attributes: state - containing - the data associated with that reaction, metabolite or gene; and el - a - HTML node that you can render content in. If you need to manage state - for your tooltip, you can alternatively pass a tinier component. See + (Default: ``escher.Tooltip.DefaultTooltip``) A `Preact`_ Component to + show when hoving over reactions, metabolites, and genes. See ``escher.Tooltip.DefaultTooltip`` in the source code for an example of - a tinier component that defines the default tooltips. + a Preact component that defines the default tooltips. And see the + :doc:`Tooltip Tutorial ` for more tips on + getting started with custom tooltips. .. js:attribute:: options.enable_tooltips - (Default: `[`label`]`) Determines the mouseover or touch event required - to show the related tooltip.['label'] will show tooltips upon mouseover - or touch of the reaction or metabolite labels whereas ['object'] will - show the the tooltips over the reaction line segments and metabolite - circles. Can be set as an empty array to disable tooltips or can have - both options passed in to enable tooltips over both labels and objects. + (Default: ``['label']``) Determines the mouseover or touch + event required to show the related tooltip.['label'] will show + tooltips upon mouseover or touch of the reaction or metabolite labels + whereas ['object'] will show the the tooltips over the reaction line + segments and metabolite circles. Can be set as an empty array to + disable tooltips or can have both options passed in to enable tooltips + over both labels and objects. + + .. js:attribute:: options.enable_keys_with_tooltip + + (Default: ``true``) Set this to ``false`` to disallow non-prefixed + (i.e. not starting with ctrl or cmd) keyboard shortcuts when the + tooltip is visible. This is useful if your tooltip has text inputs. **Callbacks** .. js:attribute:: options.first_load_callback - A function to run after loading the Builder. + A function to run after loading the Builder. The Builder instance is + passed as a single argument to the callback. .. **Callbacks** @@ -397,4 +401,4 @@ JavaScript API turn of the gene_reaction_rules. .. _`support the 3D transforms`: http://caniuse.com/#feat=transforms3d -.. _`tinier`: https://github.com/zakandrewking/tinier +.. _`Preact`: https://preactjs.com/ diff --git a/docs/notebooks/Escher Widget Example.ipynb b/docs/notebooks/Escher Widget Example.ipynb new file mode 100644 index 00000000..ce12acd7 --- /dev/null +++ b/docs/notebooks/Escher Widget Example.ipynb @@ -0,0 +1,161 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 41, + "metadata": {}, + "outputs": [], + "source": [ + "from escher import Builder" + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": {}, + "outputs": [], + "source": [ + "import cobra" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": {}, + "outputs": [], + "source": [ + "core = cobra.io.load_json_model('/Users/zaking/Downloads/e_coli_core.json')" + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": {}, + "outputs": [], + "source": [ + "builder = Builder(height=600, map_name=None, model_name='iJO1366', model_json='https://escher.github.io/1-0-0/5/models/Escherichia%20coli/e_coli_core.json')" + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "d6adebc2155f456d9bf24f45910a2d0c", + "version_major": 2, + "version_minor": 0 + }, + "text/html": [ + "

Failed to display Jupyter Widget of type Builder.

\n", + "

\n", + " If you're reading this message in the Jupyter Notebook or JupyterLab Notebook, it may mean\n", + " that the widgets JavaScript is still loading. If this message persists, it\n", + " likely means that the widgets JavaScript library is either not installed or\n", + " not enabled. See the Jupyter\n", + " Widgets Documentation for setup instructions.\n", + "

\n", + "

\n", + " If you're reading this message in another frontend (for example, a static\n", + " rendering on GitHub or NBViewer),\n", + " it may mean that your frontend doesn't currently support widgets.\n", + "

\n" + ], + "text/plain": [ + "Builder(height=600)" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "builder" + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "metadata": {}, + "outputs": [], + "source": [ + "builder.map_json = 'https://escher.github.io/1-0-0/5/maps/Escherichia%20coli/e_coli_core.Core%20metabolism.json'" + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "metadata": {}, + "outputs": [], + "source": [ + "builder.model = core" + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "metadata": {}, + "outputs": [], + "source": [ + "builder.height = 500" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'[{\"map_name\": \"e_coli_core.Core metabolism\", \"map_id\": \"0df3827fde8464e80f455a773a52c274\", \"map_description\": \"E. coli core metabolic network\\\\nLast Modified Fri Dec 05 2014 16:39:44 GMT-0800 (PST)\", \"homepage\": \"https://escher.github.io\", \"schema\": \"https://escher.github.io/escher/jsonschema/1-0-0#\"}, {\"reactions\": {\"1577514\": {\"name\": \"Formate transport in via proton symport\", \"bigg_id\": \"FORt2\", \"segments\": {\"11\": {\"to_node_id\": \"1577044\", \"from_node_id\": \"1576666\", \"b2\": {\"y\": 4749.209372712298, \"x\": 1360.0}, \"b1\": {\"y\": 4827.353507999329, \"x\": 1360.0}}, \"10\": {\"to_node_id\": \"1577046\", \"from_node_id\": \"1577045\", \"b2\": null, \"b1\": null}, \"13\": {\"to_node_id\": \"1576590\", \"from_node_id\": \"1577046\", \"b2\": {\"y\": 4275.94781629675, \"x\": 1360.0}, \"b1\": {\"y\": 4411.784344889025, \"x\": 1360.0}}, \"12\": {\"to_node_id\": \"1577044\", \"from_node_id\": \"1576833\", \"b2\": {\"y\": 4744.773286702694, \"x\": 1360.0}, \"b1\": {\"y\": 4779.244289008981, \"x\": 1360.0}}, \"14\": {\"to_node_id\": \"1576811\", \"from_node_id\": \"1577046\", \"b2\": {\"y\": 4422.5658350974745, \"x\": 1360.0}, \"b1\": {\"y\": 4455.769750529243, \"x\": 1360.0}}, \"9\": {\"to_node_id\": \"1577045\", \"from_node_id\": \"1577044\", \"b2\": null, \"b1\": null}}, \"genes\": [{\"bigg_id\": \"b2492\", \"name\": \"focB\"}, {\"bigg_id\": \"b0904\", \"name\": \"focA\"}], \"reversibility\": false, \"metabolites\": [{\"coefficient\": -1.0, \"bigg_id\": \"for_e\"}, {\"coefficient\": 1.0, \"bigg_id\": \"h_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"for_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"h_e\"}], \"label_x\": 1255.7530517578125, \"label_y\": 4574.13232421875, \"gene_reaction_rule\": \"b0904 or b2492\"}, \"1577518\": {\"name\": \"Formate transport via diffusion\", \"bigg_id\": \"FORti\", \"segments\": {\"15\": {\"to_node_id\": \"1577047\", \"from_node_id\": \"1576590\", \"b2\": {\"y\": 4263.5, \"x\": 1460.0}, \"b1\": {\"y\": 4145.55, \"x\": 1460.0}}, \"16\": {\"to_node_id\": \"1576666\", \"from_node_id\": \"1577047\", \"b2\": {\"y\": 4654.322265625, \"x\": 1460.0}, \"b1\": {\"y\": 4488.7, \"x\": 1460.0}}}, \"genes\": [{\"bigg_id\": \"b2492\", \"name\": \"focB\"}, {\"bigg_id\": \"b0904\", \"name\": \"focA\"}], \"reversibility\": false, \"metabolites\": [{\"coefficient\": 1.0, \"bigg_id\": \"for_e\"}, {\"coefficient\": -1.0, \"bigg_id\": \"for_c\"}], \"label_x\": 1479.5206298828125, \"label_y\": 4509.27197265625, \"gene_reaction_rule\": \"b0904 or b2492\"}, \"1576747\": {\"name\": \"D-lactate dehydrogenase\", \"bigg_id\": \"LDH_D\", \"segments\": {\"120\": {\"to_node_id\": \"1576957\", \"from_node_id\": \"1576956\", \"b2\": null, \"b1\": null}, \"121\": {\"to_node_id\": \"1576955\", \"from_node_id\": \"1576957\", \"b2\": null, \"b1\": null}, \"122\": {\"to_node_id\": \"1576956\", \"from_node_id\": \"1576633\", \"b2\": {\"y\": 4240.660459763366, \"x\": 1055.0}, \"b1\": {\"y\": 4277.201532544553, \"x\": 1055.0}}, \"123\": {\"to_node_id\": \"1576956\", \"from_node_id\": \"1576584\", \"b2\": {\"y\": 4243.0, \"x\": 1055.0}, \"b1\": {\"y\": 4285.0, \"x\": 1055.0}}, \"124\": {\"to_node_id\": \"1576634\", \"from_node_id\": \"1576955\", \"b2\": {\"y\": 4060.9687576256715, \"x\": 1055.0}, \"b1\": {\"y\": 4105.790627287702, \"x\": 1055.0}}, \"125\": {\"to_node_id\": \"1576511\", \"from_node_id\": \"1576955\", \"b2\": {\"y\": 4035.0, \"x\": 1055.0}, \"b1\": {\"y\": 4098.0, \"x\": 1055.0}}, \"126\": {\"to_node_id\": \"1576635\", \"from_node_id\": \"1576955\", \"b2\": {\"y\": 4072.7984674554473, \"x\": 1055.0}, \"b1\": {\"y\": 4109.339540236634, \"x\": 1055.0}}}, \"genes\": [{\"bigg_id\": \"b1380\", \"name\": \"ldhA\"}, {\"bigg_id\": \"b2133\", \"name\": \"dld\"}], \"reversibility\": true, \"metabolites\": [{\"coefficient\": 1.0, \"bigg_id\": \"pyr_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"h_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"lac__D_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"nad_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"nadh_c\"}], \"label_x\": 1065.0, \"label_y\": 4165.0, \"gene_reaction_rule\": \"b2133 or b1380\"}, \"1576746\": {\"name\": \"ATP maintenance requirement\", \"bigg_id\": \"ATPM\", \"segments\": {\"115\": {\"to_node_id\": \"1576952\", \"from_node_id\": \"1576534\", \"b2\": {\"y\": 1176.0, \"x\": 4215.0}, \"b1\": {\"y\": 1120.0, \"x\": 4215.0}}, \"114\": {\"to_node_id\": \"1576954\", \"from_node_id\": \"1576953\", \"b2\": null, \"b1\": null}, \"117\": {\"to_node_id\": \"1576631\", \"from_node_id\": \"1576954\", \"b2\": {\"y\": 1411.0327780786686, \"x\": 4215.0}, \"b1\": {\"y\": 1368.3098334236006, \"x\": 4215.0}}, \"116\": {\"to_node_id\": \"1576952\", \"from_node_id\": \"1576630\", \"b2\": {\"y\": 1187.2720779386423, \"x\": 4215.0}, \"b1\": {\"y\": 1157.5735931288073, \"x\": 4215.0}}, \"113\": {\"to_node_id\": \"1576953\", \"from_node_id\": \"1576952\", \"b2\": null, \"b1\": null}, \"119\": {\"to_node_id\": \"1576536\", \"from_node_id\": \"1576954\", \"b2\": {\"y\": 1430.0, \"x\": 4215.0}, \"b1\": {\"y\": 1374.0, \"x\": 4215.0}}, \"118\": {\"to_node_id\": \"1576632\", \"from_node_id\": \"1576954\", \"b2\": {\"y\": 1388.0788655293195, \"x\": 4215.0}, \"b1\": {\"y\": 1361.423659658796, \"x\": 4215.0}}}, \"genes\": [], \"reversibility\": false, \"metabolites\": [{\"coefficient\": 1.0, \"bigg_id\": \"adp_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"h_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"atp_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"h2o_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"pi_c\"}], \"label_x\": 4241.11474609375, \"label_y\": 1266.1151123046875, \"gene_reaction_rule\": \"\"}, \"1576745\": {\"name\": \"Glutaminase\", \"bigg_id\": \"GLUN\", \"segments\": {\"108\": {\"to_node_id\": \"1576949\", \"from_node_id\": \"1576951\", \"b2\": null, \"b1\": null}, \"109\": {\"to_node_id\": \"1576950\", \"from_node_id\": \"1576628\", \"b2\": {\"y\": 3888.700961498954, \"x\": 4247.0}, \"b1\": {\"y\": 3916.003204996513, \"x\": 4247.0}}, \"111\": {\"to_node_id\": \"1576629\", \"from_node_id\": \"1576949\", \"b2\": {\"y\": 3777.1681351239463, \"x\": 4247.0}, \"b1\": {\"y\": 3803.650440537184, \"x\": 4247.0}}, \"110\": {\"to_node_id\": \"1576950\", \"from_node_id\": \"1576578\", \"b2\": {\"y\": 3909.550345620285, \"x\": 4247.0}, \"b1\": {\"y\": 3985.501152067616, \"x\": 4247.0}}, \"112\": {\"to_node_id\": \"1576554\", \"from_node_id\": \"1576949\", \"b2\": {\"y\": 3723.4986338899794, \"x\": 4247.0}, \"b1\": {\"y\": 3787.5495901669938, \"x\": 4247.0}}, \"107\": {\"to_node_id\": \"1576951\", \"from_node_id\": \"1576950\", \"b2\": null, \"b1\": null}}, \"genes\": [{\"bigg_id\": \"b1524\", \"name\": \"glsB\"}, {\"bigg_id\": \"b0485\", \"name\": \"glsA\"}, {\"bigg_id\": \"b1812\", \"name\": \"pabB\"}], \"reversibility\": false, \"metabolites\": [{\"coefficient\": 1.0, \"bigg_id\": \"glu__L_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"gln__L_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"nh4_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"h2o_c\"}], \"label_x\": 4253.77685546875, \"label_y\": 3866.338134765625, \"gene_reaction_rule\": \"b1812 or b0485 or b1524\"}, \"1576744\": {\"name\": \"Acetate exchange\", \"bigg_id\": \"EX_ac_e\", \"segments\": {\"105\": {\"to_node_id\": \"1576947\", \"from_node_id\": \"1576627\", \"b2\": {\"y\": 4890.0, \"x\": 1715.0}, \"b1\": {\"y\": 4888.64794921875, \"x\": 1715.0}}}, \"genes\": [], \"reversibility\": false, \"metabolites\": [{\"coefficient\": -1.0, \"bigg_id\": \"ac_e\"}], \"label_x\": 1742.4544677734375, \"label_y\": 4917.06640625, \"gene_reaction_rule\": \"\"}, \"1576743\": {\"name\": \"Glucose 6-phosphate dehydrogenase\", \"bigg_id\": \"G6PDH2r\", \"segments\": {\"99\": {\"to_node_id\": \"1576944\", \"from_node_id\": \"1576945\", \"b2\": null, \"b1\": null}, \"98\": {\"to_node_id\": \"1576945\", \"from_node_id\": \"1576946\", \"b2\": null, \"b1\": null}, \"102\": {\"to_node_id\": \"1576624\", \"from_node_id\": \"1576944\", \"b2\": {\"y\": 1265.0, \"x\": 1393.0018939122203}, \"b1\": {\"y\": 1265.0, \"x\": 1346.800568173666}}, \"103\": {\"to_node_id\": \"1576625\", \"from_node_id\": \"1576944\", \"b2\": {\"y\": 1265.0, \"x\": 1381.6008241696038}, \"b1\": {\"y\": 1265.0, \"x\": 1343.3802472508812}}, \"100\": {\"to_node_id\": \"1576946\", \"from_node_id\": \"1576623\", \"b2\": {\"y\": 1265.0, \"x\": 1270.6197527491188}, \"b1\": {\"y\": 1265.0, \"x\": 1232.3991758303962}}, \"101\": {\"to_node_id\": \"1576946\", \"from_node_id\": \"1576524\", \"b2\": {\"y\": 1265.0, \"x\": 1252.2}, \"b1\": {\"y\": 1265.0, \"x\": 1171.0}}, \"104\": {\"to_node_id\": \"1576626\", \"from_node_id\": \"1576944\", \"b2\": {\"y\": 1265.0, \"x\": 1417.0}, \"b1\": {\"y\": 1265.0, \"x\": 1354.0}}}, \"genes\": [{\"bigg_id\": \"b1852\", \"name\": \"zwf\"}], \"reversibility\": true, \"metabolites\": [{\"coefficient\": -1.0, \"bigg_id\": \"g6p_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"h_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"nadp_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"nadph_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"6pgl_c\"}], \"label_x\": 1261.46337890625, \"label_y\": 1320.0572509765625, \"gene_reaction_rule\": \"b1852\"}, \"1576742\": {\"name\": \"Malic enzyme (NAD)\", \"bigg_id\": \"ME1\", \"segments\": {\"91\": {\"to_node_id\": \"1576942\", \"from_node_id\": \"1576941\", \"b2\": null, \"b1\": null}, \"93\": {\"to_node_id\": \"1576941\", \"from_node_id\": \"1576504\", \"b2\": {\"y\": 3307.175835174097, \"x\": 2355.6483296518063}, \"b1\": {\"y\": 3253.9194505803225, \"x\": 2462.161098839355}}, \"92\": {\"to_node_id\": \"1576943\", \"from_node_id\": \"1576942\", \"b2\": null, \"b1\": null}, \"95\": {\"to_node_id\": \"1576511\", \"from_node_id\": \"1576943\", \"b2\": {\"y\": 3870.856276574083, \"x\": 1117.2138384533964}, \"b1\": {\"y\": 3474.7533185191, \"x\": 2020.4933629618001}}, \"94\": {\"to_node_id\": \"1576941\", \"from_node_id\": \"1576620\", \"b2\": {\"y\": 3321.8944463483363, \"x\": 2326.2111073033275}, \"b1\": {\"y\": 3302.9814878277875, \"x\": 2364.0370243444254}}, \"97\": {\"to_node_id\": \"1576622\", \"from_node_id\": \"1576943\", \"b2\": {\"y\": 3415.0, \"x\": 2140.0}, \"b1\": {\"y\": 3397.5, \"x\": 2175.0}}, \"96\": {\"to_node_id\": \"1576621\", \"from_node_id\": \"1576943\", \"b2\": {\"y\": 3419.8328677803524, \"x\": 2130.3342644392947}, \"b1\": {\"y\": 3398.949860334106, \"x\": 2172.1002793317884}}}, \"genes\": [{\"bigg_id\": \"b1479\", \"name\": \"maeA\"}], \"reversibility\": false, \"metabolites\": [{\"coefficient\": -1.0, \"bigg_id\": \"mal__L_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"pyr_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"co2_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"nad_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"nadh_c\"}], \"label_x\": 2188.595703125, \"label_y\": 3321.438232421875, \"gene_reaction_rule\": \"b1479\"}, \"1576741\": {\"name\": \"GLNabc\", \"bigg_id\": \"GLNabc\", \"segments\": {\"88\": {\"to_node_id\": \"1576617\", \"from_node_id\": \"1576938\", \"b2\": {\"y\": 4158.0, \"x\": 4332.993902665717}, \"b1\": {\"y\": 4158.0, \"x\": 4390.398170799715}}, \"89\": {\"to_node_id\": \"1576618\", \"from_node_id\": \"1576938\", \"b2\": {\"y\": 4158.0, \"x\": 4362.798467455447}, \"b1\": {\"y\": 4158.0, \"x\": 4399.339540236634}}, \"90\": {\"to_node_id\": \"1576619\", \"from_node_id\": \"1576938\", \"b2\": {\"y\": 4158.0, \"x\": 4350.968757625671}, \"b1\": {\"y\": 4158.0, \"x\": 4395.790627287702}}, \"82\": {\"to_node_id\": \"1576939\", \"from_node_id\": \"1576940\", \"b2\": null, \"b1\": null}, \"83\": {\"to_node_id\": \"1576938\", \"from_node_id\": \"1576939\", \"b2\": null, \"b1\": null}, \"86\": {\"to_node_id\": \"1576940\", \"from_node_id\": \"1576616\", \"b2\": {\"y\": 4158.0, \"x\": 4470.660459763366}, \"b1\": {\"y\": 4158.0, \"x\": 4507.201532544553}}, \"87\": {\"to_node_id\": \"1576578\", \"from_node_id\": \"1576938\", \"b2\": {\"y\": 4158.0, \"x\": 4324.643760591755}, \"b1\": {\"y\": 4158.0, \"x\": 4387.893128177527}}, \"84\": {\"to_node_id\": \"1576940\", \"from_node_id\": \"1576537\", \"b2\": {\"y\": 4158.0, \"x\": 4519.05}, \"b1\": {\"y\": 4158.0, \"x\": 4668.5}}, \"85\": {\"to_node_id\": \"1576940\", \"from_node_id\": \"1576615\", \"b2\": {\"y\": 4158.0, \"x\": 4474.209372712298}, \"b1\": {\"y\": 4158.0, \"x\": 4519.031242374329}}}, \"genes\": [{\"bigg_id\": \"b0811\", \"name\": \"glnH\"}, {\"bigg_id\": \"b0810\", \"name\": \"glnP\"}, {\"bigg_id\": \"b0809\", \"name\": \"glnQ\"}], \"reversibility\": false, \"metabolites\": [{\"coefficient\": -1.0, \"bigg_id\": \"h2o_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"atp_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"gln__L_e\"}, {\"coefficient\": 1.0, \"bigg_id\": \"adp_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"h_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"pi_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"gln__L_c\"}], \"label_x\": 4535.24462890625, \"label_y\": 4133.49658203125, \"gene_reaction_rule\": \"b0811 and b0810 and b0809\"}, \"1577547\": {\"name\": \"Biomass Objective Function with GAM\", \"bigg_id\": \"BIOMASS_Ecoli_core_w_GAM\", \"segments\": {\"498\": {\"to_node_id\": \"1577090\", \"from_node_id\": \"1577076\", \"b2\": {\"y\": 4899.2412109375, \"x\": 5215.0458984375}, \"b1\": {\"y\": 4793.48193359375, \"x\": 5334.0517578125}}, \"499\": {\"to_node_id\": \"1577091\", \"from_node_id\": \"1577076\", \"b2\": {\"y\": 4977.43115234375, \"x\": 5353.5947265625}, \"b1\": {\"y\": 4789.48193359375, \"x\": 5334.0517578125}}, \"494\": {\"to_node_id\": \"1577086\", \"from_node_id\": \"1577074\", \"b2\": {\"y\": 4564.25048828125, \"x\": 5140.27099609375}, \"b1\": {\"y\": 4681.48193359375, \"x\": 5334.0517578125}}, \"495\": {\"to_node_id\": \"1577087\", \"from_node_id\": \"1577076\", \"b2\": {\"y\": 4938.26416015625, \"x\": 5273.55517578125}, \"b1\": {\"y\": 4793.48193359375, \"x\": 5334.0517578125}}, \"496\": {\"to_node_id\": \"1577088\", \"from_node_id\": \"1577074\", \"b2\": {\"y\": 4579.03759765625, \"x\": 5068.69580078125}, \"b1\": {\"y\": 4681.48193359375, \"x\": 5334.0517578125}}, \"497\": {\"to_node_id\": \"1577089\", \"from_node_id\": \"1577074\", \"b2\": {\"y\": 4594.88134765625, \"x\": 5023.52734375}, \"b1\": {\"y\": 4681.48193359375, \"x\": 5334.0517578125}}, \"490\": {\"to_node_id\": \"1577082\", \"from_node_id\": \"1577074\", \"b2\": {\"y\": 4413.09912109375, \"x\": 5307.541015625}, \"b1\": {\"y\": 4681.48193359375, \"x\": 5334.0517578125}}, \"491\": {\"to_node_id\": \"1577083\", \"from_node_id\": \"1577074\", \"b2\": {\"y\": 4523.05615234375, \"x\": 5316.9716796875}, \"b1\": {\"y\": 4681.48193359375, \"x\": 5334.0517578125}}, \"492\": {\"to_node_id\": \"1577084\", \"from_node_id\": \"1577074\", \"b2\": {\"y\": 4533.61865234375, \"x\": 5258.0712890625}, \"b1\": {\"y\": 4681.48193359375, \"x\": 5334.0517578125}}, \"493\": {\"to_node_id\": \"1577085\", \"from_node_id\": \"1577074\", \"b2\": {\"y\": 4544.18115234375, \"x\": 5197.05859375}, \"b1\": {\"y\": 4681.48193359375, \"x\": 5334.0517578125}}, \"506\": {\"to_node_id\": \"1577098\", \"from_node_id\": \"1577076\", \"b2\": {\"y\": 4820.185546875, \"x\": 5099.533203125}, \"b1\": {\"y\": 4793.48193359375, \"x\": 5334.0517578125}}, \"504\": {\"to_node_id\": \"1577096\", \"from_node_id\": \"1577076\", \"b2\": {\"y\": 4868.333984375, \"x\": 5114.79541015625}, \"b1\": {\"y\": 4793.48193359375, \"x\": 5334.0517578125}}, \"505\": {\"to_node_id\": \"1577097\", \"from_node_id\": \"1577074\", \"b2\": {\"y\": 4717.4072265625, \"x\": 4780.78271484375}, \"b1\": {\"y\": 4681.48193359375, \"x\": 5334.0517578125}}, \"502\": {\"to_node_id\": \"1577094\", \"from_node_id\": \"1577074\", \"b2\": {\"y\": 4664.59423828125, \"x\": 4851.0517578125}, \"b1\": {\"y\": 4681.48193359375, \"x\": 5334.0517578125}}, \"503\": {\"to_node_id\": \"1577095\", \"from_node_id\": \"1577074\", \"b2\": {\"y\": 4685.7197265625, \"x\": 4807.9951171875}, \"b1\": {\"y\": 4681.48193359375, \"x\": 5334.0517578125}}, \"500\": {\"to_node_id\": \"1577092\", \"from_node_id\": \"1577074\", \"b2\": {\"y\": 4611.78173828125, \"x\": 4962.51416015625}, \"b1\": {\"y\": 4681.48193359375, \"x\": 5334.0517578125}}, \"501\": {\"to_node_id\": \"1577093\", \"from_node_id\": \"1577074\", \"b2\": {\"y\": 4637.1318359375, \"x\": 4907.83935546875}, \"b1\": {\"y\": 4681.48193359375, \"x\": 5334.0517578125}}, \"489\": {\"to_node_id\": \"1577081\", \"from_node_id\": \"1577076\", \"b2\": {\"y\": 5001.37158203125, \"x\": 5337.27587890625}, \"b1\": {\"y\": 4793.48193359375, \"x\": 5334.0517578125}}, \"488\": {\"to_node_id\": \"1577080\", \"from_node_id\": \"1577074\", \"b2\": {\"y\": 4453.01611328125, \"x\": 5350.98828125}, \"b1\": {\"y\": 4681.48193359375, \"x\": 5334.0517578125}}, \"487\": {\"to_node_id\": \"1577079\", \"from_node_id\": \"1577074\", \"b2\": {\"y\": 4516.16943359375, \"x\": 5423.4208984375}, \"b1\": {\"y\": 4681.48193359375, \"x\": 5334.0517578125}}, \"486\": {\"to_node_id\": \"1577078\", \"from_node_id\": \"1577074\", \"b2\": {\"y\": 4630.19921875, \"x\": 5427.28515625}, \"b1\": {\"y\": 4681.48193359375, \"x\": 5334.0517578125}}, \"485\": {\"to_node_id\": \"1577077\", \"from_node_id\": \"1577076\", \"b2\": {\"y\": 4926.6220703125, \"x\": 5391.197265625}, \"b1\": {\"y\": 4793.48193359375, \"x\": 5334.0517578125}}, \"484\": {\"to_node_id\": \"1577073\", \"from_node_id\": \"1577074\", \"b2\": {\"y\": 4573.30322265625, \"x\": 5455.328125}, \"b1\": {\"y\": 4685.48193359375, \"x\": 5334.0517578125}}, \"483\": {\"to_node_id\": \"1577075\", \"from_node_id\": \"1577076\", \"b2\": null, \"b1\": null}, \"482\": {\"to_node_id\": \"1577075\", \"from_node_id\": \"1577074\", \"b2\": null, \"b1\": null}}, \"genes\": [], \"reversibility\": false, \"metabolites\": [{\"coefficient\": 3.7478, \"bigg_id\": \"coa_c\"}, {\"coefficient\": -0.8977, \"bigg_id\": \"r5p_c\"}, {\"coefficient\": -1.496, \"bigg_id\": \"3pg_c\"}, {\"coefficient\": -0.361, \"bigg_id\": \"e4p_c\"}, {\"coefficient\": -0.2557, \"bigg_id\": \"gln__L_c\"}, {\"coefficient\": 3.547, \"bigg_id\": \"nadh_c\"}, {\"coefficient\": -2.8328, \"bigg_id\": \"pyr_c\"}, {\"coefficient\": -0.0709, \"bigg_id\": \"f6p_c\"}, {\"coefficient\": -13.0279, \"bigg_id\": \"nadph_c\"}, {\"coefficient\": -3.7478, \"bigg_id\": \"accoa_c\"}, {\"coefficient\": -1.7867, \"bigg_id\": \"oaa_c\"}, {\"coefficient\": -0.129, \"bigg_id\": \"g3p_c\"}, {\"coefficient\": -3.547, \"bigg_id\": \"nad_c\"}, {\"coefficient\": 59.81, \"bigg_id\": \"pi_c\"}, {\"coefficient\": -0.205, \"bigg_id\": \"g6p_c\"}, {\"coefficient\": 4.1182, \"bigg_id\": \"akg_c\"}, {\"coefficient\": -0.5191, \"bigg_id\": \"pep_c\"}, {\"coefficient\": -4.9414, \"bigg_id\": \"glu__L_c\"}, {\"coefficient\": -59.81, \"bigg_id\": \"atp_c\"}, {\"coefficient\": 59.81, \"bigg_id\": \"adp_c\"}, {\"coefficient\": 59.81, \"bigg_id\": \"h_c\"}, {\"coefficient\": -59.81, \"bigg_id\": \"h2o_c\"}, {\"coefficient\": 13.0279, \"bigg_id\": \"nadp_c\"}], \"label_x\": 5392.94580078125, \"label_y\": 4746.6962890625, \"gene_reaction_rule\": \"\"}, \"1576749\": {\"name\": \"H2O transport via diffusion\", \"bigg_id\": \"H2Ot\", \"segments\": {\"135\": {\"to_node_id\": \"1576640\", \"from_node_id\": \"1576961\", \"b2\": {\"y\": 4524.0, \"x\": 3257.0}, \"b1\": {\"y\": 4587.0, \"x\": 3257.0}}, \"134\": {\"to_node_id\": \"1576961\", \"from_node_id\": \"1576639\", \"b2\": {\"y\": 4684.0, \"x\": 3257.0}, \"b1\": {\"y\": 4733.0, \"x\": 3257.0}}}, \"genes\": [{\"bigg_id\": \"s0001\", \"name\": \"None\"}, {\"bigg_id\": \"b0875\", \"name\": \"aqpZ\"}], \"reversibility\": true, \"metabolites\": [{\"coefficient\": 1.0, \"bigg_id\": \"h2o_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"h2o_e\"}], \"label_x\": 3267.0, \"label_y\": 4604.0, \"gene_reaction_rule\": \"b0875 or s0001\"}, \"1576748\": {\"name\": \"Malate dehydrogenase\", \"bigg_id\": \"MDH\", \"segments\": {\"133\": {\"to_node_id\": \"1576521\", \"from_node_id\": \"1576958\", \"b2\": {\"y\": 3595.960728582294, \"x\": 2580.6915524727365}, \"b1\": {\"y\": 3497.9882185746883, \"x\": 2578.807465741821}}, \"132\": {\"to_node_id\": \"1576638\", \"from_node_id\": \"1576958\", \"b2\": {\"y\": 3505.0826757349405, \"x\": 2578.9438976102874}, \"b1\": {\"y\": 3470.7248027204823, \"x\": 2578.2831692830864}}, \"131\": {\"to_node_id\": \"1576637\", \"from_node_id\": \"1576958\", \"b2\": {\"y\": 3538.2116199944217, \"x\": 2579.5809926922}, \"b1\": {\"y\": 3480.6634859983265, \"x\": 2578.47429780766}}, \"130\": {\"to_node_id\": \"1576960\", \"from_node_id\": \"1576636\", \"b2\": {\"y\": 3327.0487806839114, \"x\": 2583.107239628261}, \"b1\": {\"y\": 3294.4959356130375, \"x\": 2585.69079876087}}, \"127\": {\"to_node_id\": \"1576959\", \"from_node_id\": \"1576960\", \"b2\": null, \"b1\": null}, \"128\": {\"to_node_id\": \"1576958\", \"from_node_id\": \"1576959\", \"b2\": null, \"b1\": null}, \"129\": {\"to_node_id\": \"1576960\", \"from_node_id\": \"1576504\", \"b2\": {\"y\": 3317.969496697346, \"x\": 2583.827817722433}, \"b1\": {\"y\": 3264.23165565782, \"x\": 2588.092725741443}}}, \"genes\": [{\"bigg_id\": \"b3236\", \"name\": \"mdh\"}], \"reversibility\": true, \"metabolites\": [{\"coefficient\": -1.0, \"bigg_id\": \"mal__L_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"h_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"oaa_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"nad_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"nadh_c\"}], \"label_x\": 2587.0, \"label_y\": 3394.0, \"gene_reaction_rule\": \"b3236\"}, \"1577545\": {\"name\": \"Succinate dehydrogenase (irreversible)\", \"bigg_id\": \"SUCDi\", \"segments\": {\"48\": {\"to_node_id\": \"1577066\", \"from_node_id\": \"1577065\", \"b2\": null, \"b1\": null}, \"49\": {\"to_node_id\": \"1577064\", \"from_node_id\": \"1576814\", \"b2\": {\"y\": 2816.4590794794085, \"x\": 3193.7934732750823}, \"b1\": {\"y\": 2815.1969315980286, \"x\": 3225.9782442502737}}, \"47\": {\"to_node_id\": \"1577065\", \"from_node_id\": \"1577064\", \"b2\": null, \"b1\": null}, \"51\": {\"to_node_id\": \"1576503\", \"from_node_id\": \"1577066\", \"b2\": {\"y\": 2843.2084583195547, \"x\": 2952.624089386102}, \"b1\": {\"y\": 2831.162537495866, \"x\": 3040.3872268158307}}, \"50\": {\"to_node_id\": \"1577064\", \"from_node_id\": \"1576533\", \"b2\": {\"y\": 2815.5590310361767, \"x\": 3216.7447085774984}, \"b1\": {\"y\": 2812.1967701205886, \"x\": 3302.482361924995}}, \"52\": {\"to_node_id\": \"1576830\", \"from_node_id\": \"1577066\", \"b2\": {\"y\": 2833.0526323200284, \"x\": 3026.6165359540787}, \"b1\": {\"y\": 2828.1157896960085, \"x\": 3062.5849607862237}}}, \"genes\": [{\"bigg_id\": \"b0724\", \"name\": \"sdhB\"}, {\"bigg_id\": \"b0721\", \"name\": \"sdhC\"}, {\"bigg_id\": \"b0722\", \"name\": \"sdhD\"}, {\"bigg_id\": \"b0723\", \"name\": \"sdhA\"}], \"reversibility\": false, \"metabolites\": [{\"coefficient\": -1.0, \"bigg_id\": \"succ_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"fum_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"q8_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"q8h2_c\"}], \"label_x\": 3108.851318359375, \"label_y\": 2862.949951171875, \"gene_reaction_rule\": \"b0721 and b0722 and b0723 and b0724\"}, \"1577540\": {\"name\": \"NADH dehydrogenase (ubiquinone-8 & 3 protons)\", \"bigg_id\": \"NADH16\", \"segments\": {\"39\": {\"to_node_id\": \"1577062\", \"from_node_id\": \"1577061\", \"b2\": null, \"b1\": null}, \"46\": {\"to_node_id\": \"1576542\", \"from_node_id\": \"1577063\", \"b2\": {\"y\": 2507.151123046875, \"x\": 4630.938294107013}, \"b1\": {\"y\": 2571.611572265625, \"x\": 4800.053949169604}}, \"44\": {\"to_node_id\": \"1576808\", \"from_node_id\": \"1577063\", \"b2\": {\"y\": 2570.0, \"x\": 4780.0}, \"b1\": {\"y\": 2568.388427734375, \"x\": 4766.45361328125}}, \"45\": {\"to_node_id\": \"1576809\", \"from_node_id\": \"1577063\", \"b2\": {\"y\": 2655.409912109375, \"x\": 4928.099683314913}, \"b1\": {\"y\": 2570.000244140625, \"x\": 4798.6355690569735}}, \"42\": {\"to_node_id\": \"1577061\", \"from_node_id\": \"1576827\", \"b2\": {\"y\": 2570.0, \"x\": 4595.729021023612}, \"b1\": {\"y\": 2567.302001953125, \"x\": 4458.799047943291}}, \"43\": {\"to_node_id\": \"1577061\", \"from_node_id\": \"1576539\", \"b2\": {\"y\": 2570.0, \"x\": 4542.513923505967}, \"b1\": {\"y\": 2375.007080078125, \"x\": 4706.342147363639}}, \"40\": {\"to_node_id\": \"1577063\", \"from_node_id\": \"1577062\", \"b2\": null, \"b1\": null}, \"41\": {\"to_node_id\": \"1577061\", \"from_node_id\": \"1576820\", \"b2\": {\"y\": 2570.0, \"x\": 4582.0}, \"b1\": {\"y\": 2570.0, \"x\": 4470.0}}}, \"genes\": [{\"bigg_id\": \"b2284\", \"name\": \"nuoF\"}, {\"bigg_id\": \"b2279\", \"name\": \"nuoK\"}, {\"bigg_id\": \"b2287\", \"name\": \"nuoB\"}, {\"bigg_id\": \"b2286\", \"name\": \"nuoC\"}, {\"bigg_id\": \"b2288\", \"name\": \"nuoA\"}, {\"bigg_id\": \"b2283\", \"name\": \"nuoG\"}, {\"bigg_id\": \"b2277\", \"name\": \"nuoM\"}, {\"bigg_id\": \"b2281\", \"name\": \"nuoI\"}, {\"bigg_id\": \"b2280\", \"name\": \"nuoJ\"}, {\"bigg_id\": \"b2276\", \"name\": \"nuoN\"}, {\"bigg_id\": \"b2282\", \"name\": \"nuoH\"}, {\"bigg_id\": \"b2278\", \"name\": \"nuoL\"}, {\"bigg_id\": \"b2285\", \"name\": \"nuoE\"}], \"reversibility\": false, \"metabolites\": [{\"coefficient\": 3.0, \"bigg_id\": \"h_e\"}, {\"coefficient\": -1.0, \"bigg_id\": \"q8_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"nadh_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"q8h2_c\"}, {\"coefficient\": -4.0, \"bigg_id\": \"h_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"nad_c\"}], \"label_x\": 4589.75537109375, \"label_y\": 2624.46044921875, \"gene_reaction_rule\": \"b2276 and b2277 and b2278 and b2279 and b2280 and b2281 and b2282 and b2283 and b2284 and b2285 and b2286 and b2287 and b2288\"}, \"1577507\": {\"name\": \"Alcohol dehydrogenase (ethanol)\", \"bigg_id\": \"ALCD2x\", \"segments\": {\"3\": {\"to_node_id\": \"1577043\", \"from_node_id\": \"1577042\", \"b2\": null, \"b1\": null}, \"2\": {\"to_node_id\": \"1577042\", \"from_node_id\": \"1577041\", \"b2\": null, \"b1\": null}, \"5\": {\"to_node_id\": \"1577041\", \"from_node_id\": \"1576822\", \"b2\": {\"y\": 4111.911975251582, \"x\": 2383.7628306266947}, \"b1\": {\"y\": 4139.7065841719395, \"x\": 2413.5427687556494}}, \"4\": {\"to_node_id\": \"1577041\", \"from_node_id\": \"1576829\", \"b2\": {\"y\": 4118.6722220553565, \"x\": 2391.0059522021684}, \"b1\": {\"y\": 4162.240740184523, \"x\": 2437.6865073405606}}, \"7\": {\"to_node_id\": \"1576813\", \"from_node_id\": \"1577043\", \"b2\": {\"y\": 4033.046682944248, \"x\": 2306.576904081298}, \"b1\": {\"y\": 4059.6140048832744, \"x\": 2331.3730712243896}}, \"6\": {\"to_node_id\": \"1576817\", \"from_node_id\": \"1577043\", \"b2\": {\"y\": 4004.2974661013272, \"x\": 2279.744301694572}, \"b1\": {\"y\": 4050.989239830398, \"x\": 2323.3232905083714}}, \"8\": {\"to_node_id\": \"1576824\", \"from_node_id\": \"1577043\", \"b2\": {\"y\": 4021.1795086945217, \"x\": 2295.5008747815536}, \"b1\": {\"y\": 4056.0538526083565, \"x\": 2328.0502624344663}}}, \"genes\": [{\"bigg_id\": \"b1241\", \"name\": \"adhE\"}, {\"bigg_id\": \"b0356\", \"name\": \"frmA\"}, {\"bigg_id\": \"b1478\", \"name\": \"adhP\"}], \"reversibility\": true, \"metabolites\": [{\"coefficient\": 1.0, \"bigg_id\": \"acald_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"h_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"nad_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"nadh_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"etoh_c\"}], \"label_x\": 2356.4794921875, \"label_y\": 4047.4384765625, \"gene_reaction_rule\": \"b0356 or b1478 or b1241\"}, \"1576754\": {\"name\": \"Phosphoenolpyruvate synthase\", \"bigg_id\": \"PPS\", \"segments\": {\"151\": {\"to_node_id\": \"1576970\", \"from_node_id\": \"1576511\", \"b2\": {\"y\": 3844.115214431216, \"x\": 835.0}, \"b1\": {\"y\": 3935.384048104053, \"x\": 835.0}}, \"150\": {\"to_node_id\": \"1576970\", \"from_node_id\": \"1576642\", \"b2\": {\"y\": 3821.7705098312485, \"x\": 835.0}, \"b1\": {\"y\": 3860.9016994374947, \"x\": 835.0}}, \"153\": {\"to_node_id\": \"1576517\", \"from_node_id\": \"1576971\", \"b2\": {\"y\": 3391.8647304430565, \"x\": 835.0}, \"b1\": {\"y\": 3485.059419132917, \"x\": 835.0}}, \"152\": {\"to_node_id\": \"1576970\", \"from_node_id\": \"1576643\", \"b2\": {\"y\": 3820.0, \"x\": 835.0}, \"b1\": {\"y\": 3855.0, \"x\": 835.0}}, \"155\": {\"to_node_id\": \"1576645\", \"from_node_id\": \"1576971\", \"b2\": {\"y\": 3475.0, \"x\": 835.0}, \"b1\": {\"y\": 3510.0, \"x\": 835.0}}, \"154\": {\"to_node_id\": \"1576644\", \"from_node_id\": \"1576971\", \"b2\": {\"y\": 3469.0983005625053, \"x\": 835.0}, \"b1\": {\"y\": 3508.2294901687515, \"x\": 835.0}}, \"156\": {\"to_node_id\": \"1576646\", \"from_node_id\": \"1576971\", \"b2\": {\"y\": 3454.289321881345, \"x\": 835.0}, \"b1\": {\"y\": 3503.7867965644036, \"x\": 835.0}}, \"148\": {\"to_node_id\": \"1576972\", \"from_node_id\": \"1576970\", \"b2\": null, \"b1\": null}, \"149\": {\"to_node_id\": \"1576971\", \"from_node_id\": \"1576972\", \"b2\": null, \"b1\": null}}, \"genes\": [{\"bigg_id\": \"b1702\", \"name\": \"ppsA\"}], \"reversibility\": false, \"metabolites\": [{\"coefficient\": -1.0, \"bigg_id\": \"pyr_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"atp_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"amp_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"pep_c\"}, {\"coefficient\": 2.0, \"bigg_id\": \"h_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"pi_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"h2o_c\"}], \"label_x\": 741.8604125976562, \"label_y\": 3669.90869140625, \"gene_reaction_rule\": \"b1702\"}, \"1576755\": {\"name\": \"D-Glucose exchange\", \"bigg_id\": \"EX_glc__D_e\", \"segments\": {\"157\": {\"to_node_id\": \"1576974\", \"from_node_id\": \"1576647\", \"b2\": {\"y\": 623.4132690429688, \"x\": 1056.6529541015625}, \"b1\": {\"y\": 625.4632568359375, \"x\": 1055.066162109375}}}, \"genes\": [], \"reversibility\": true, \"metabolites\": [{\"coefficient\": -1.0, \"bigg_id\": \"glc__D_e\"}], \"label_x\": 1082.5206298828125, \"label_y\": 598.4705200195312, \"gene_reaction_rule\": \"\"}, \"1576756\": {\"name\": \"ETOHt2r\", \"bigg_id\": \"ETOHt2r\", \"segments\": {\"159\": {\"to_node_id\": \"1576976\", \"from_node_id\": \"1576977\", \"b2\": null, \"b1\": null}, \"164\": {\"to_node_id\": \"1576829\", \"from_node_id\": \"1576975\", \"b2\": {\"y\": 4328.998762383821, \"x\": 2500.0}, \"b1\": {\"y\": 4399.699628715146, \"x\": 2500.0}}, \"160\": {\"to_node_id\": \"1576975\", \"from_node_id\": \"1576976\", \"b2\": null, \"b1\": null}, \"161\": {\"to_node_id\": \"1576977\", \"from_node_id\": \"1576648\", \"b2\": {\"y\": 4745.660459763366, \"x\": 2500.0}, \"b1\": {\"y\": 4782.201532544553, \"x\": 2500.0}}, \"162\": {\"to_node_id\": \"1576977\", \"from_node_id\": \"1576649\", \"b2\": {\"y\": 4745.0, \"x\": 2500.0}, \"b1\": {\"y\": 4780.0, \"x\": 2500.0}}, \"163\": {\"to_node_id\": \"1576650\", \"from_node_id\": \"1576975\", \"b2\": {\"y\": 4377.798467455447, \"x\": 2500.0}, \"b1\": {\"y\": 4414.339540236634, \"x\": 2500.0}}}, \"genes\": [], \"reversibility\": true, \"metabolites\": [{\"coefficient\": -1.0, \"bigg_id\": \"h_e\"}, {\"coefficient\": 1.0, \"bigg_id\": \"etoh_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"etoh_e\"}, {\"coefficient\": 1.0, \"bigg_id\": \"h_c\"}], \"label_x\": 2530.6279296875, \"label_y\": 4611.404296875, \"gene_reaction_rule\": \"\"}, \"1576757\": {\"name\": \"Phosphoglycerate mutase\", \"bigg_id\": \"PGM\", \"segments\": {\"165\": {\"to_node_id\": \"1576978\", \"from_node_id\": \"1576522\", \"b2\": {\"y\": 3060.0, \"x\": 1055.0}, \"b1\": {\"y\": 3105.5, \"x\": 1055.0}}, \"166\": {\"to_node_id\": \"1576486\", \"from_node_id\": \"1576978\", \"b2\": {\"y\": 2935.0, \"x\": 1055.0}, \"b1\": {\"y\": 2977.0, \"x\": 1055.0}}}, \"genes\": [{\"bigg_id\": \"b3612\", \"name\": \"gpmM\"}, {\"bigg_id\": \"b4395\", \"name\": \"ytjC\"}, {\"bigg_id\": \"b0755\", \"name\": \"gpmA\"}], \"reversibility\": true, \"metabolites\": [{\"coefficient\": 1.0, \"bigg_id\": \"3pg_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"2pg_c\"}], \"label_x\": 1065.0, \"label_y\": 2985.0, \"gene_reaction_rule\": \"b3612 or b4395 or b0755\"}, \"1576750\": {\"name\": \"H2O exchange\", \"bigg_id\": \"EX_h2o_e\", \"segments\": {\"136\": {\"to_node_id\": \"1576962\", \"from_node_id\": \"1576639\", \"b2\": {\"y\": 4839.0, \"x\": 3257.0}, \"b1\": {\"y\": 4779.5, \"x\": 3257.0}}}, \"genes\": [], \"reversibility\": true, \"metabolites\": [{\"coefficient\": -1.0, \"bigg_id\": \"h2o_e\"}], \"label_x\": 3267.0, \"label_y\": 4914.0, \"gene_reaction_rule\": \"\"}, \"1576751\": {\"name\": \"Transketolase\", \"bigg_id\": \"TKT1\", \"segments\": {\"142\": {\"to_node_id\": \"1576546\", \"from_node_id\": \"1576966\", \"b2\": {\"y\": 1816.322080948729, \"x\": 2070.2734375}, \"b1\": {\"y\": 1823.819109636181, \"x\": 2173.413330078125}}, \"143\": {\"to_node_id\": \"1576545\", \"from_node_id\": \"1576966\", \"b2\": {\"y\": 1821.0824569252916, \"x\": 2297.1806640625}, \"b1\": {\"y\": 1822.232439714306, \"x\": 2175.0}}, \"140\": {\"to_node_id\": \"1576964\", \"from_node_id\": \"1576605\", \"b2\": {\"y\": 1605.9896167668144, \"x\": 2175.0}, \"b1\": {\"y\": 1557.80791851959, \"x\": 2171.826416015625}}, \"141\": {\"to_node_id\": \"1576964\", \"from_node_id\": \"1576558\", \"b2\": {\"y\": 1607.9527328943145, \"x\": 2175.0}, \"b1\": {\"y\": 1558.0047151164654, \"x\": 2171.82666015625}}, \"139\": {\"to_node_id\": \"1576966\", \"from_node_id\": \"1576965\", \"b2\": null, \"b1\": null}, \"138\": {\"to_node_id\": \"1576965\", \"from_node_id\": \"1576964\", \"b2\": null, \"b1\": null}}, \"genes\": [{\"bigg_id\": \"b2465\", \"name\": \"tktB\"}, {\"bigg_id\": \"b2935\", \"name\": \"tktA\"}], \"reversibility\": true, \"metabolites\": [{\"coefficient\": -1.0, \"bigg_id\": \"xu5p__D_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"r5p_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"g3p_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"s7p_c\"}], \"label_x\": 2185.0, \"label_y\": 1685.0, \"gene_reaction_rule\": \"b2935 or b2465\"}, \"1576752\": {\"name\": \"CO2 transporter via diffusion\", \"bigg_id\": \"CO2t\", \"segments\": {\"144\": {\"to_node_id\": \"1576967\", \"from_node_id\": \"1576544\", \"b2\": {\"y\": 4679.0, \"x\": 3621.0}, \"b1\": {\"y\": 4731.5, \"x\": 3621.0}}, \"145\": {\"to_node_id\": \"1576641\", \"from_node_id\": \"1576967\", \"b2\": {\"y\": 4519.0, \"x\": 3621.0}, \"b1\": {\"y\": 4578.5, \"x\": 3621.0}}}, \"genes\": [{\"bigg_id\": \"s0001\", \"name\": \"None\"}], \"reversibility\": true, \"metabolites\": [{\"coefficient\": -1.0, \"bigg_id\": \"co2_e\"}, {\"coefficient\": 1.0, \"bigg_id\": \"co2_c\"}], \"label_x\": 3631.0, \"label_y\": 4594.0, \"gene_reaction_rule\": \"s0001\"}, \"1576753\": {\"name\": \"2-Oxoglutarate exchange\", \"bigg_id\": \"EX_akg_e\", \"segments\": {\"146\": {\"to_node_id\": \"1576969\", \"from_node_id\": \"1576598\", \"b2\": {\"y\": 3228.0, \"x\": 4977.0}, \"b1\": {\"y\": 3228.0, \"x\": 4938.5}}}, \"genes\": [], \"reversibility\": false, \"metabolites\": [{\"coefficient\": -1.0, \"bigg_id\": \"akg_e\"}], \"label_x\": 5042.0, \"label_y\": 3218.0, \"gene_reaction_rule\": \"\"}, \"1576758\": {\"name\": \"Ethanol exchange\", \"bigg_id\": \"EX_etoh_e\", \"segments\": {\"167\": {\"to_node_id\": \"1576980\", \"from_node_id\": \"1576649\", \"b2\": {\"y\": 4887.93359375, \"x\": 2500.0}, \"b1\": {\"y\": 4875.1484375, \"x\": 2500.0}}}, \"genes\": [], \"reversibility\": false, \"metabolites\": [{\"coefficient\": -1.0, \"bigg_id\": \"etoh_e\"}], \"label_x\": 2519.520751953125, \"label_y\": 4934.28076171875, \"gene_reaction_rule\": \"\"}, \"1576759\": {\"name\": \"Acetate kinase\", \"bigg_id\": \"ACKr\", \"segments\": {\"169\": {\"to_node_id\": \"1576982\", \"from_node_id\": \"1576981\", \"b2\": null, \"b1\": null}, \"174\": {\"to_node_id\": \"1576613\", \"from_node_id\": \"1576983\", \"b2\": {\"y\": 4215.0, \"x\": 1715.0}, \"b1\": {\"y\": 4243.0, \"x\": 1715.0}}, \"173\": {\"to_node_id\": \"1576653\", \"from_node_id\": \"1576983\", \"b2\": {\"y\": 4202.798467455447, \"x\": 1715.0}, \"b1\": {\"y\": 4239.339540236634, \"x\": 1715.0}}, \"172\": {\"to_node_id\": \"1576981\", \"from_node_id\": \"1576652\", \"b2\": {\"y\": 4317.0, \"x\": 1715.0}, \"b1\": {\"y\": 4345.0, \"x\": 1715.0}}, \"171\": {\"to_node_id\": \"1576981\", \"from_node_id\": \"1576651\", \"b2\": {\"y\": 4320.660459763366, \"x\": 1715.0}, \"b1\": {\"y\": 4357.201532544553, \"x\": 1715.0}}, \"170\": {\"to_node_id\": \"1576983\", \"from_node_id\": \"1576982\", \"b2\": null, \"b1\": null}}, \"genes\": [{\"bigg_id\": \"b2296\", \"name\": \"ackA\"}, {\"bigg_id\": \"b3115\", \"name\": \"tdcD\"}, {\"bigg_id\": \"b1849\", \"name\": \"purT\"}], \"reversibility\": true, \"metabolites\": [{\"coefficient\": 1.0, \"bigg_id\": \"adp_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"ac_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"actp_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"atp_c\"}], \"label_x\": 1737.694091796875, \"label_y\": 4293.5615234375, \"gene_reaction_rule\": \"b3115 or b2296 or b1849\"}, \"1576710\": {\"name\": \"CO2 exchange\", \"bigg_id\": \"EX_co2_e\", \"segments\": {\"367\": {\"to_node_id\": \"1576871\", \"from_node_id\": \"1576544\", \"b2\": {\"y\": 4834.0, \"x\": 3621.0}, \"b1\": {\"y\": 4778.0, \"x\": 3621.0}}}, \"genes\": [], \"reversibility\": true, \"metabolites\": [{\"coefficient\": -1.0, \"bigg_id\": \"co2_e\"}], \"label_x\": 3631.0, \"label_y\": 4904.0, \"gene_reaction_rule\": \"\"}, \"1576760\": {\"name\": \"Ammonia exchange\", \"bigg_id\": \"EX_nh4_e\", \"segments\": {\"175\": {\"to_node_id\": \"1576985\", \"from_node_id\": \"1576526\", \"b2\": {\"y\": 4837.5, \"x\": 3990.0}, \"b1\": {\"y\": 4781.15, \"x\": 3989.3}}}, \"genes\": [], \"reversibility\": true, \"metabolites\": [{\"coefficient\": -1.0, \"bigg_id\": \"nh4_e\"}], \"label_x\": 4001.0, \"label_y\": 4908.0, \"gene_reaction_rule\": \"\"}, \"1576763\": {\"name\": \"Fumarate transport via proton symport 2 H \", \"bigg_id\": \"FUMt2_2\", \"segments\": {\"191\": {\"to_node_id\": \"1576659\", \"from_node_id\": \"1576992\", \"b2\": {\"y\": 1032.2015325445527, \"x\": 2840.0}, \"b1\": {\"y\": 995.6604597633658, \"x\": 2840.0}}, \"190\": {\"to_node_id\": \"1576991\", \"from_node_id\": \"1576658\", \"b2\": {\"y\": 714.3395402366342, \"x\": 2840.0}, \"b1\": {\"y\": 677.7984674554473, \"x\": 2840.0}}, \"192\": {\"to_node_id\": \"1576503\", \"from_node_id\": \"1576992\", \"b2\": {\"y\": 1950.0011597931211, \"x\": 2840.0}, \"b1\": {\"y\": 1271.0003479379363, \"x\": 2840.0}}, \"187\": {\"to_node_id\": \"1576993\", \"from_node_id\": \"1576991\", \"b2\": null, \"b1\": null}, \"188\": {\"to_node_id\": \"1576992\", \"from_node_id\": \"1576993\", \"b2\": null, \"b1\": null}, \"189\": {\"to_node_id\": \"1576991\", \"from_node_id\": \"1576657\", \"b2\": {\"y\": 718.0, \"x\": 2840.0}, \"b1\": {\"y\": 690.0, \"x\": 2840.0}}}, \"genes\": [{\"bigg_id\": \"b3528\", \"name\": \"dctA\"}], \"reversibility\": false, \"metabolites\": [{\"coefficient\": 1.0, \"bigg_id\": \"fum_c\"}, {\"coefficient\": 2.0, \"bigg_id\": \"h_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"fum_e\"}, {\"coefficient\": -2.0, \"bigg_id\": \"h_e\"}], \"label_x\": 2851.586669921875, \"label_y\": 845.8676147460938, \"gene_reaction_rule\": \"b3528\"}, \"1576762\": {\"name\": \"Phosphate exchange\", \"bigg_id\": \"EX_pi_e\", \"segments\": {\"185\": {\"to_node_id\": \"1576989\", \"from_node_id\": \"1576594\", \"b2\": {\"y\": 4909.82177734375, \"x\": 2880.0}, \"b1\": {\"y\": 4896.2666015625, \"x\": 2879.999755859375}}}, \"genes\": [], \"reversibility\": true, \"metabolites\": [{\"coefficient\": -1.0, \"bigg_id\": \"pi_e\"}], \"label_x\": 2902.694091796875, \"label_y\": 4926.21484375, \"gene_reaction_rule\": \"\"}, \"1576765\": {\"name\": \"D-glucose transport via PEP:Pyr PTS\", \"bigg_id\": \"GLCpts\", \"segments\": {\"201\": {\"to_node_id\": \"1576998\", \"from_node_id\": \"1576662\", \"b2\": {\"y\": 830.8607140720374, \"x\": 1056.2982974892807}, \"b1\": {\"y\": 817.6128620109578, \"x\": 962.100078544998}}, \"200\": {\"to_node_id\": \"1576999\", \"from_node_id\": \"1576997\", \"b2\": null, \"b1\": null}, \"203\": {\"to_node_id\": \"1576663\", \"from_node_id\": \"1576999\", \"b2\": {\"y\": 1091.99109457335, \"x\": 1162.89990234375}, \"b1\": {\"y\": 1155.6497636747395, \"x\": 1056.5867919921875}}, \"202\": {\"to_node_id\": \"1576998\", \"from_node_id\": \"1576647\", \"b2\": {\"y\": 852.0646158583277, \"x\": 1054.6540059434642}, \"b1\": {\"y\": 841.218336226978, \"x\": 1054.4691636917553}}, \"199\": {\"to_node_id\": \"1576997\", \"from_node_id\": \"1576998\", \"b2\": null, \"b1\": null}, \"204\": {\"to_node_id\": \"1576524\", \"from_node_id\": \"1576999\", \"b2\": {\"y\": 1159.3526000976562, \"x\": 1055.0}, \"b1\": {\"y\": 1156.4998046875, \"x\": 1056.5867919921875}}}, \"genes\": [{\"bigg_id\": \"b1621\", \"name\": \"malX\"}, {\"bigg_id\": \"b2416\", \"name\": \"ptsI\"}, {\"bigg_id\": \"b1817\", \"name\": \"manX\"}, {\"bigg_id\": \"b1101\", \"name\": \"ptsG\"}, {\"bigg_id\": \"b1818\", \"name\": \"manY\"}, {\"bigg_id\": \"b1819\", \"name\": \"manZ\"}, {\"bigg_id\": \"b2415\", \"name\": \"ptsH\"}, {\"bigg_id\": \"b2417\", \"name\": \"crr\"}], \"reversibility\": false, \"metabolites\": [{\"coefficient\": -1.0, \"bigg_id\": \"pep_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"pyr_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"glc__D_e\"}, {\"coefficient\": 1.0, \"bigg_id\": \"g6p_c\"}], \"label_x\": 1074.520263671875, \"label_y\": 929.0499877929688, \"gene_reaction_rule\": \"(b2417 and b1101 and b2415 and b2416) or (b1817 and b1818 and b1819 and b2415 and b2416) or (b2417 and b1621 and b2415 and b2416)\"}, \"1576764\": {\"name\": \"Acetate reversible transport via proton symport\", \"bigg_id\": \"ACt2r\", \"segments\": {\"198\": {\"to_node_id\": \"1576661\", \"from_node_id\": \"1576996\", \"b2\": {\"y\": 4430.253057299197, \"x\": 1719.76025390625}, \"b1\": {\"y\": 4485.835145705384, \"x\": 1715.0}}, \"195\": {\"to_node_id\": \"1576994\", \"from_node_id\": \"1576627\", \"b2\": {\"y\": 4760.0, \"x\": 1715.0}, \"b1\": {\"y\": 4779.13232421875, \"x\": 1716.5867919921875}}, \"194\": {\"to_node_id\": \"1576996\", \"from_node_id\": \"1576995\", \"b2\": null, \"b1\": null}, \"197\": {\"to_node_id\": \"1576652\", \"from_node_id\": \"1576996\", \"b2\": {\"y\": 4450.38818359375, \"x\": 1715.0}, \"b1\": {\"y\": 4489.49560546875, \"x\": 1715.0}}, \"196\": {\"to_node_id\": \"1576994\", \"from_node_id\": \"1576660\", \"b2\": {\"y\": 4760.660459763366, \"x\": 1715.0}, \"b1\": {\"y\": 4787.680536450803, \"x\": 1715.0}}, \"193\": {\"to_node_id\": \"1576995\", \"from_node_id\": \"1576994\", \"b2\": null, \"b1\": null}}, \"genes\": [], \"reversibility\": true, \"metabolites\": [{\"coefficient\": -1.0, \"bigg_id\": \"h_e\"}, {\"coefficient\": 1.0, \"bigg_id\": \"h_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"ac_e\"}, {\"coefficient\": 1.0, \"bigg_id\": \"ac_c\"}], \"label_x\": 1740.8675537109375, \"label_y\": 4638.470703125, \"gene_reaction_rule\": \"\"}, \"1576766\": {\"name\": \"Succinate transport out via proton antiport\", \"bigg_id\": \"SUCCt3\", \"segments\": {\"205\": {\"to_node_id\": \"1577000\", \"from_node_id\": \"1577002\", \"b2\": null, \"b1\": null}, \"207\": {\"to_node_id\": \"1577002\", \"from_node_id\": \"1576664\", \"b2\": {\"y\": 2844.388427734375, \"x\": 4392.512762612581}, \"b1\": {\"y\": 2928.186767578125, \"x\": 4395.992900114855}}, \"210\": {\"to_node_id\": \"1576665\", \"from_node_id\": \"1577001\", \"b2\": {\"y\": 2757.366943359375, \"x\": 4896.373608481642}, \"b1\": {\"y\": 2844.388427734375, \"x\": 4895.4172094976175}}, \"206\": {\"to_node_id\": \"1577001\", \"from_node_id\": \"1577000\", \"b2\": null, \"b1\": null}, \"209\": {\"to_node_id\": \"1576531\", \"from_node_id\": \"1577001\", \"b2\": {\"y\": 2846.0, \"x\": 4852.764726786234}, \"b1\": {\"y\": 2844.388427734375, \"x\": 4830.28303131712}}, \"208\": {\"to_node_id\": \"1577002\", \"from_node_id\": \"1576533\", \"b2\": {\"y\": 2846.0, \"x\": 4388.970989302984}, \"b1\": {\"y\": 2846.0, \"x\": 3989.9032976766134}}}, \"genes\": [], \"reversibility\": false, \"metabolites\": [{\"coefficient\": -1.0, \"bigg_id\": \"succ_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"succ_e\"}, {\"coefficient\": 1.0, \"bigg_id\": \"h_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"h_e\"}], \"label_x\": 4451.38818359375, \"label_y\": 2819.884765625, \"gene_reaction_rule\": \"\"}, \"1576769\": {\"name\": \"Glyceraldehyde-3-phosphate dehydrogenase\", \"bigg_id\": \"GAPD\", \"segments\": {\"217\": {\"to_node_id\": \"1577008\", \"from_node_id\": \"1577006\", \"b2\": null, \"b1\": null}, \"218\": {\"to_node_id\": \"1577007\", \"from_node_id\": \"1577008\", \"b2\": null, \"b1\": null}, \"219\": {\"to_node_id\": \"1577006\", \"from_node_id\": \"1576575\", \"b2\": {\"y\": 2322.5, \"x\": 1055.0}, \"b1\": {\"y\": 2270.0, \"x\": 1055.0}}, \"224\": {\"to_node_id\": \"1576672\", \"from_node_id\": \"1577007\", \"b2\": {\"y\": 2482.4341649025255, \"x\": 1055.0}, \"b1\": {\"y\": 2449.230249470758, \"x\": 1055.0}}, \"223\": {\"to_node_id\": \"1576671\", \"from_node_id\": \"1577007\", \"b2\": {\"y\": 2495.2079728939616, \"x\": 1055.0}, \"b1\": {\"y\": 2453.0623918681886, \"x\": 1055.0}}, \"222\": {\"to_node_id\": \"1576487\", \"from_node_id\": \"1577007\", \"b2\": {\"y\": 2500.0, \"x\": 1055.0}, \"b1\": {\"y\": 2454.5, \"x\": 1055.0}}, \"221\": {\"to_node_id\": \"1577006\", \"from_node_id\": \"1576670\", \"b2\": {\"y\": 2330.769750529242, \"x\": 1055.0}, \"b1\": {\"y\": 2297.5658350974745, \"x\": 1055.0}}, \"220\": {\"to_node_id\": \"1577006\", \"from_node_id\": \"1576669\", \"b2\": {\"y\": 2326.9376081318114, \"x\": 1055.0}, \"b1\": {\"y\": 2284.7920271060384, \"x\": 1055.0}}}, \"genes\": [{\"bigg_id\": \"b1779\", \"name\": \"gapA\"}], \"reversibility\": true, \"metabolites\": [{\"coefficient\": 1.0, \"bigg_id\": \"13dpg_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"g3p_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"nadh_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"h_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"nad_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"pi_c\"}], \"label_x\": 1065.0, \"label_y\": 2385.0, \"gene_reaction_rule\": \"b1779\"}, \"1576768\": {\"name\": \"6-phosphogluconolactonase\", \"bigg_id\": \"PGL\", \"segments\": {\"216\": {\"to_node_id\": \"1576490\", \"from_node_id\": \"1577004\", \"b2\": {\"y\": 1265.0, \"x\": 1742.0}, \"b1\": {\"y\": 1265.0, \"x\": 1682.5}}, \"214\": {\"to_node_id\": \"1577005\", \"from_node_id\": \"1576667\", \"b2\": {\"y\": 1265.0, \"x\": 1600.6197527491188}, \"b1\": {\"y\": 1265.0, \"x\": 1562.3991758303962}}, \"215\": {\"to_node_id\": \"1576668\", \"from_node_id\": \"1577004\", \"b2\": {\"y\": 1265.0, \"x\": 1711.6008241696038}, \"b1\": {\"y\": 1265.0, \"x\": 1673.3802472508812}}, \"212\": {\"to_node_id\": \"1577004\", \"from_node_id\": \"1577003\", \"b2\": null, \"b1\": null}, \"213\": {\"to_node_id\": \"1577005\", \"from_node_id\": \"1576626\", \"b2\": {\"y\": 1265.0, \"x\": 1600.5}, \"b1\": {\"y\": 1265.0, \"x\": 1562.0}}, \"211\": {\"to_node_id\": \"1577003\", \"from_node_id\": \"1577005\", \"b2\": null, \"b1\": null}}, \"genes\": [{\"bigg_id\": \"b0767\", \"name\": \"pgl\"}], \"reversibility\": false, \"metabolites\": [{\"coefficient\": -1.0, \"bigg_id\": \"6pgl_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"h_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"h2o_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"6pgc_c\"}], \"label_x\": 1599.3970947265625, \"label_y\": 1312.1234130859375, \"gene_reaction_rule\": \"b0767\"}, \"1576699\": {\"name\": \"Pyruvate dehydrogenase\", \"bigg_id\": \"PDH\", \"segments\": {\"331\": {\"to_node_id\": \"1576514\", \"from_node_id\": \"1576851\", \"b2\": {\"y\": 3945.0, \"x\": 1635.0}, \"b1\": {\"y\": 3945.0, \"x\": 1579.0}}, \"330\": {\"to_node_id\": \"1576513\", \"from_node_id\": \"1576851\", \"b2\": {\"y\": 3945.0, \"x\": 1606.478150704935}, \"b1\": {\"y\": 3945.0, \"x\": 1570.4434452114806}}, \"332\": {\"to_node_id\": \"1576515\", \"from_node_id\": \"1576851\", \"b2\": {\"y\": 3945.0, \"x\": 1613.309518948453}, \"b1\": {\"y\": 3945.0, \"x\": 1572.492855684536}}, \"326\": {\"to_node_id\": \"1576851\", \"from_node_id\": \"1576850\", \"b2\": null, \"b1\": null}, \"327\": {\"to_node_id\": \"1576849\", \"from_node_id\": \"1576510\", \"b2\": {\"y\": 3945.0, \"x\": 1413.3472975166192}, \"b1\": {\"y\": 3945.0, \"x\": 1374.4909917220637}}, \"325\": {\"to_node_id\": \"1576850\", \"from_node_id\": \"1576849\", \"b2\": null, \"b1\": null}, \"328\": {\"to_node_id\": \"1576849\", \"from_node_id\": \"1576511\", \"b2\": {\"y\": 3945.0, \"x\": 1373.75}, \"b1\": {\"y\": 3945.0, \"x\": 1242.5}}, \"329\": {\"to_node_id\": \"1576849\", \"from_node_id\": \"1576512\", \"b2\": {\"y\": 3945.0, \"x\": 1411.736306507171}, \"b1\": {\"y\": 3945.0, \"x\": 1369.1210216905704}}}, \"genes\": [{\"bigg_id\": \"b0114\", \"name\": \"aceE\"}, {\"bigg_id\": \"b0115\", \"name\": \"aceF\"}, {\"bigg_id\": \"b0116\", \"name\": \"lpd\"}], \"reversibility\": false, \"metabolites\": [{\"coefficient\": 1.0, \"bigg_id\": \"nadh_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"coa_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"pyr_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"accoa_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"nad_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"co2_c\"}], \"label_x\": 1417.72802734375, \"label_y\": 3920.71923828125, \"gene_reaction_rule\": \"b0114 and b0115 and b0116\"}, \"1576698\": {\"name\": \"Isocitrate dehydrogenase (NADP)\", \"bigg_id\": \"ICDHyr\", \"segments\": {\"319\": {\"to_node_id\": \"1576848\", \"from_node_id\": \"1576846\", \"b2\": null, \"b1\": null}, \"318\": {\"to_node_id\": \"1576846\", \"from_node_id\": \"1576847\", \"b2\": null, \"b1\": null}, \"322\": {\"to_node_id\": \"1576507\", \"from_node_id\": \"1576848\", \"b2\": {\"y\": 3735.4876397766343, \"x\": 3702.9108389651055}, \"b1\": {\"y\": 3767.3462919329904, \"x\": 3674.9732516895315}}, \"323\": {\"to_node_id\": \"1576508\", \"from_node_id\": \"1576848\", \"b2\": {\"y\": 3749.7955576929353, \"x\": 3690.36389556158}, \"b1\": {\"y\": 3771.6386673078805, \"x\": 3671.209168668474}}, \"320\": {\"to_node_id\": \"1576847\", \"from_node_id\": \"1576505\", \"b2\": {\"y\": 3933.673891941737, \"x\": 3498.9695345126847}, \"b1\": {\"y\": 3958.5796398057905, \"x\": 3468.565115042282}}, \"321\": {\"to_node_id\": \"1576847\", \"from_node_id\": \"1576506\", \"b2\": {\"y\": 3936.593890513596, \"x\": 3495.4048609314546}, \"b1\": {\"y\": 3968.312968378653, \"x\": 3456.6828697715146}}, \"324\": {\"to_node_id\": \"1576509\", \"from_node_id\": \"1576848\", \"b2\": {\"y\": 3715.2337610392046, \"x\": 3720.671932627159}, \"b1\": {\"y\": 3761.2701283117613, \"x\": 3680.3015797881476}}}, \"genes\": [{\"bigg_id\": \"b1136\", \"name\": \"icd\"}], \"reversibility\": true, \"metabolites\": [{\"coefficient\": 1.0, \"bigg_id\": \"nadph_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"akg_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"co2_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"nadp_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"icit_c\"}], \"label_x\": 3616.0, \"label_y\": 3836.0, \"gene_reaction_rule\": \"b1136\"}, \"1576695\": {\"name\": \"O2 transport diffusion \", \"bigg_id\": \"O2t\", \"segments\": {\"304\": {\"to_node_id\": \"1576840\", \"from_node_id\": \"1576494\", \"b2\": {\"y\": 1660.0, \"x\": 4755.0}, \"b1\": {\"y\": 1660.0, \"x\": 4821.5}}, \"305\": {\"to_node_id\": \"1576495\", \"from_node_id\": \"1576840\", \"b2\": {\"y\": 1660.0, \"x\": 4495.0}, \"b1\": {\"y\": 1660.0, \"x\": 4610.5}}}, \"genes\": [{\"bigg_id\": \"s0001\", \"name\": \"None\"}], \"reversibility\": true, \"metabolites\": [{\"coefficient\": 1.0, \"bigg_id\": \"o2_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"o2_e\"}], \"label_x\": 4557.1943359375, \"label_y\": 1635.4964599609375, \"gene_reaction_rule\": \"s0001\"}, \"1576694\": {\"name\": \"Phosphogluconate dehydrogenase\", \"bigg_id\": \"GND\", \"segments\": {\"300\": {\"to_node_id\": \"1576838\", \"from_node_id\": \"1576490\", \"b2\": {\"y\": 1265.0, \"x\": 1920.5}, \"b1\": {\"y\": 1265.0, \"x\": 1882.0}}, \"301\": {\"to_node_id\": \"1576491\", \"from_node_id\": \"1576839\", \"b2\": {\"y\": 1265.0, \"x\": 2029.2015325445527}, \"b1\": {\"y\": 1265.0, \"x\": 1992.660459763366}}, \"302\": {\"to_node_id\": \"1576492\", \"from_node_id\": \"1576839\", \"b2\": {\"y\": 1265.0, \"x\": 2041.0312423743285}, \"b1\": {\"y\": 1265.0, \"x\": 1996.2093727122985}}, \"303\": {\"to_node_id\": \"1576493\", \"from_node_id\": \"1576839\", \"b2\": {\"y\": 1265.0, \"x\": 2066.0}, \"b1\": {\"y\": 1265.0, \"x\": 2003.7}}, \"298\": {\"to_node_id\": \"1576839\", \"from_node_id\": \"1576837\", \"b2\": null, \"b1\": null}, \"299\": {\"to_node_id\": \"1576838\", \"from_node_id\": \"1576489\", \"b2\": {\"y\": 1265.0, \"x\": 1921.339540236634}, \"b1\": {\"y\": 1265.0, \"x\": 1884.7984674554473}}, \"297\": {\"to_node_id\": \"1576837\", \"from_node_id\": \"1576838\", \"b2\": null, \"b1\": null}}, \"genes\": [{\"bigg_id\": \"b2029\", \"name\": \"gnd\"}], \"reversibility\": false, \"metabolites\": [{\"coefficient\": 1.0, \"bigg_id\": \"ru5p__D_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"nadph_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"co2_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"nadp_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"6pgc_c\"}], \"label_x\": 1930.5045166015625, \"label_y\": 1313.710205078125, \"gene_reaction_rule\": \"b2029\"}, \"1576697\": {\"name\": \"Fumarase\", \"bigg_id\": \"FUM\", \"segments\": {\"317\": {\"to_node_id\": \"1576844\", \"from_node_id\": \"1576503\", \"b2\": {\"y\": 3003.8745504813855, \"x\": 2741.433228320378}, \"b1\": {\"y\": 2961.5818349379524, \"x\": 2775.110761067927}}, \"316\": {\"to_node_id\": \"1576844\", \"from_node_id\": \"1576502\", \"b2\": {\"y\": 3010.0650991921693, \"x\": 2736.5037173099395}, \"b1\": {\"y\": 2982.216997307231, \"x\": 2758.679057699798}}, \"315\": {\"to_node_id\": \"1576504\", \"from_node_id\": \"1576845\", \"b2\": {\"y\": 3134.0, \"x\": 2652.5}, \"b1\": {\"y\": 3093.4, \"x\": 2674.55}}, \"314\": {\"to_node_id\": \"1576845\", \"from_node_id\": \"1576844\", \"b2\": null, \"b1\": null}}, \"genes\": [{\"bigg_id\": \"b4122\", \"name\": \"fumB\"}, {\"bigg_id\": \"b1611\", \"name\": \"fumC\"}, {\"bigg_id\": \"b1612\", \"name\": \"fumA\"}], \"reversibility\": true, \"metabolites\": [{\"coefficient\": -1.0, \"bigg_id\": \"fum_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"h2o_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"mal__L_c\"}], \"label_x\": 2759.057373046875, \"label_y\": 3066.0, \"gene_reaction_rule\": \"b1612 or b4122 or b1611\"}, \"1576696\": {\"name\": \"NAD P transhydrogenase\", \"bigg_id\": \"THD2\", \"segments\": {\"308\": {\"to_node_id\": \"1576842\", \"from_node_id\": \"1576496\", \"b2\": {\"y\": 724.5066228070302, \"x\": 3510.0}, \"b1\": {\"y\": 580.5856339661426, \"x\": 3331.1220703125}}, \"309\": {\"to_node_id\": \"1576842\", \"from_node_id\": \"1576497\", \"b2\": {\"y\": 750.137689584216, \"x\": 3510.0}, \"b1\": {\"y\": 654.7419446101471, \"x\": 3418.143798828125}}, \"313\": {\"to_node_id\": \"1576501\", \"from_node_id\": \"1576843\", \"b2\": {\"y\": 1081.817596989458, \"x\": 3624.41748046875}, \"b1\": {\"y\": 1038.7769502394156, \"x\": 3506.777099609375}}, \"312\": {\"to_node_id\": \"1576500\", \"from_node_id\": \"1576843\", \"b2\": {\"y\": 1078.144282674624, \"x\": 3740.4462890625}, \"b1\": {\"y\": 1061.6865465211372, \"x\": 3508.388671875}}, \"311\": {\"to_node_id\": \"1576499\", \"from_node_id\": \"1576843\", \"b2\": {\"y\": 995.287841796875, \"x\": 3510.0}, \"b1\": {\"y\": 921.2841796875, \"x\": 3510.0}}, \"310\": {\"to_node_id\": \"1576842\", \"from_node_id\": \"1576498\", \"b2\": {\"y\": 777.5, \"x\": 3510.0}, \"b1\": {\"y\": 725.0, \"x\": 3510.0}}, \"306\": {\"to_node_id\": \"1576841\", \"from_node_id\": \"1576842\", \"b2\": null, \"b1\": null}, \"307\": {\"to_node_id\": \"1576843\", \"from_node_id\": \"1576841\", \"b2\": null, \"b1\": null}}, \"genes\": [{\"bigg_id\": \"b1602\", \"name\": \"pntB\"}, {\"bigg_id\": \"b1603\", \"name\": \"pntA\"}], \"reversibility\": false, \"metabolites\": [{\"coefficient\": -2.0, \"bigg_id\": \"h_e\"}, {\"coefficient\": -1.0, \"bigg_id\": \"nadp_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"nadh_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"nadph_c\"}, {\"coefficient\": 2.0, \"bigg_id\": \"h_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"nad_c\"}], \"label_x\": 3532.891845703125, \"label_y\": 872.8489990234375, \"gene_reaction_rule\": \"b1602 and b1603\"}, \"1576693\": {\"name\": \"Phosphoglycerate kinase\", \"bigg_id\": \"PGK\", \"segments\": {\"296\": {\"to_node_id\": \"1576488\", \"from_node_id\": \"1576834\", \"b2\": {\"y\": 2617.5658350974745, \"x\": 1055.0}, \"b1\": {\"y\": 2650.769750529242, \"x\": 1055.0}}, \"294\": {\"to_node_id\": \"1576835\", \"from_node_id\": \"1576486\", \"b2\": {\"y\": 2790.0, \"x\": 1055.0}, \"b1\": {\"y\": 2825.0, \"x\": 1055.0}}, \"295\": {\"to_node_id\": \"1576487\", \"from_node_id\": \"1576834\", \"b2\": {\"y\": 2615.0, \"x\": 1055.0}, \"b1\": {\"y\": 2650.0, \"x\": 1055.0}}, \"292\": {\"to_node_id\": \"1576834\", \"from_node_id\": \"1576836\", \"b2\": null, \"b1\": null}, \"293\": {\"to_node_id\": \"1576835\", \"from_node_id\": \"1576485\", \"b2\": {\"y\": 2789.230249470758, \"x\": 1055.0}, \"b1\": {\"y\": 2822.4341649025255, \"x\": 1055.0}}, \"291\": {\"to_node_id\": \"1576836\", \"from_node_id\": \"1576835\", \"b2\": null, \"b1\": null}}, \"genes\": [{\"bigg_id\": \"b2926\", \"name\": \"pgk\"}], \"reversibility\": true, \"metabolites\": [{\"coefficient\": -1.0, \"bigg_id\": \"3pg_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"13dpg_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"atp_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"adp_c\"}], \"label_x\": 1065.0, \"label_y\": 2715.0, \"gene_reaction_rule\": \"b2926\"}, \"1576776\": {\"name\": \"Malic enzyme (NADP)\", \"bigg_id\": \"ME2\", \"segments\": {\"270\": {\"to_node_id\": \"1576692\", \"from_node_id\": \"1577027\", \"b2\": {\"y\": 3439.4108823397055, \"x\": 2161.178235320589}, \"b1\": {\"y\": 3418.8232647019117, \"x\": 2202.3534705961765}}, \"267\": {\"to_node_id\": \"1577028\", \"from_node_id\": \"1576504\", \"b2\": {\"y\": 3332.720745600581, \"x\": 2355.4654239990323}, \"b1\": {\"y\": 3269.069152001936, \"x\": 2461.551413330107}}, \"266\": {\"to_node_id\": \"1577028\", \"from_node_id\": \"1576690\", \"b2\": {\"y\": 3350.7069760509485, \"x\": 2325.4883732484195}, \"b1\": {\"y\": 3329.0232535031614, \"x\": 2361.6279108280646}}, \"265\": {\"to_node_id\": \"1577027\", \"from_node_id\": \"1577026\", \"b2\": null, \"b1\": null}, \"264\": {\"to_node_id\": \"1577026\", \"from_node_id\": \"1577028\", \"b2\": null, \"b1\": null}, \"269\": {\"to_node_id\": \"1576511\", \"from_node_id\": \"1577027\", \"b2\": {\"y\": 3895.002675936253, \"x\": 1164.309223322806}, \"b1\": {\"y\": 3495.997238327751, \"x\": 2048.005523344498}}, \"268\": {\"to_node_id\": \"1576691\", \"from_node_id\": \"1577027\", \"b2\": {\"y\": 3434.0831891575845, \"x\": 2171.8336216848306}, \"b1\": {\"y\": 3417.2249567472754, \"x\": 2205.550086505449}}}, \"genes\": [{\"bigg_id\": \"b2463\", \"name\": \"maeB\"}], \"reversibility\": false, \"metabolites\": [{\"coefficient\": 1.0, \"bigg_id\": \"pyr_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"mal__L_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"co2_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"nadp_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"nadph_c\"}], \"label_x\": 2281.107177734375, \"label_y\": 3432.36328125, \"gene_reaction_rule\": \"b2463\"}, \"1576777\": {\"name\": \"Formate exchange\", \"bigg_id\": \"EX_for_e\", \"segments\": {\"271\": {\"to_node_id\": \"1577030\", \"from_node_id\": \"1576666\", \"b2\": {\"y\": 4884.95458984375, \"x\": 1460.0}, \"b1\": {\"y\": 4885.96044921875, \"x\": 1461.5867919921875}}}, \"genes\": [], \"reversibility\": false, \"metabolites\": [{\"coefficient\": -1.0, \"bigg_id\": \"for_e\"}], \"label_x\": 1476.3470458984375, \"label_y\": 4913.4130859375, \"gene_reaction_rule\": \"\"}, \"1576774\": {\"name\": \"Citrate synthase\", \"bigg_id\": \"CS\", \"segments\": {\"252\": {\"to_node_id\": \"1577020\", \"from_node_id\": \"1576514\", \"b2\": {\"y\": 3745.4027250468985, \"x\": 2627.479742988546}, \"b1\": {\"y\": 3679.773731927161, \"x\": 1727.7705707040077}}, \"253\": {\"to_node_id\": \"1577020\", \"from_node_id\": \"1576684\", \"b2\": {\"y\": 3823.2954829227556, \"x\": 2728.8970628128327}, \"b1\": {\"y\": 3791.3182764091857, \"x\": 2693.656876042776}}, \"250\": {\"to_node_id\": \"1577022\", \"from_node_id\": \"1577020\", \"b2\": null, \"b1\": null}, \"251\": {\"to_node_id\": \"1577021\", \"from_node_id\": \"1577022\", \"b2\": null, \"b1\": null}, \"256\": {\"to_node_id\": \"1576686\", \"from_node_id\": \"1577021\", \"b2\": {\"y\": 3971.425553937104, \"x\": 2910.1210395424487}, \"b1\": {\"y\": 3934.0276661811313, \"x\": 2861.0363118627347}}, \"257\": {\"to_node_id\": \"1576687\", \"from_node_id\": \"1577021\", \"b2\": {\"y\": 3980.7262047116856, \"x\": 2922.328143684087}, \"b1\": {\"y\": 3936.817861413506, \"x\": 2864.698443105226}}, \"254\": {\"to_node_id\": \"1577020\", \"from_node_id\": \"1576521\", \"b2\": {\"y\": 3822.7470798256522, \"x\": 2728.292700216025}, \"b1\": {\"y\": 3789.4902660855073, \"x\": 2691.642334053416}}, \"255\": {\"to_node_id\": \"1576685\", \"from_node_id\": \"1577021\", \"b2\": {\"y\": 3962.7252091426644, \"x\": 2898.7018369997472}, \"b1\": {\"y\": 3931.4175627427994, \"x\": 2857.610551099924}}}, \"genes\": [{\"bigg_id\": \"b0720\", \"name\": \"gltA\"}], \"reversibility\": false, \"metabolites\": [{\"coefficient\": 1.0, \"bigg_id\": \"coa_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"oaa_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"h_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"cit_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"accoa_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"h2o_c\"}], \"label_x\": 2808.0, \"label_y\": 3876.0, \"gene_reaction_rule\": \"b0720\"}, \"1576775\": {\"name\": \"Pyruvate transport in via proton symport\", \"bigg_id\": \"PYRt2\", \"segments\": {\"260\": {\"to_node_id\": \"1577023\", \"from_node_id\": \"1576688\", \"b2\": {\"y\": 3945.0, \"x\": 359.3395402366342}, \"b1\": {\"y\": 3945.0, \"x\": 322.7984674554473}}, \"261\": {\"to_node_id\": \"1577023\", \"from_node_id\": \"1576557\", \"b2\": {\"y\": 3945.0, \"x\": 360.0}, \"b1\": {\"y\": 3945.0, \"x\": 325.0}}, \"263\": {\"to_node_id\": \"1576689\", \"from_node_id\": \"1577024\", \"b2\": {\"y\": 3945.0, \"x\": 697.2015325445527}, \"b1\": {\"y\": 3945.0, \"x\": 660.6604597633658}}, \"262\": {\"to_node_id\": \"1576511\", \"from_node_id\": \"1577024\", \"b2\": {\"y\": 3945.0, \"x\": 850.0}, \"b1\": {\"y\": 3945.0, \"x\": 706.5}}, \"258\": {\"to_node_id\": \"1577025\", \"from_node_id\": \"1577023\", \"b2\": null, \"b1\": null}, \"259\": {\"to_node_id\": \"1577024\", \"from_node_id\": \"1577025\", \"b2\": null, \"b1\": null}}, \"genes\": [], \"reversibility\": true, \"metabolites\": [{\"coefficient\": 1.0, \"bigg_id\": \"pyr_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"h_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"pyr_e\"}, {\"coefficient\": -1.0, \"bigg_id\": \"h_e\"}], \"label_x\": 462.6368408203125, \"label_y\": 3908.02490234375, \"gene_reaction_rule\": \"\"}, \"1576772\": {\"name\": \"Glutamate dehydrogenase (NADP)\", \"bigg_id\": \"GLUDy\", \"segments\": {\"238\": {\"to_node_id\": \"1576677\", \"from_node_id\": \"1577014\", \"b2\": {\"y\": 3632.0, \"x\": 4021.2725815852687}, \"b1\": {\"y\": 3632.0, \"x\": 4060.9817744755806}}, \"240\": {\"to_node_id\": \"1576509\", \"from_node_id\": \"1577014\", \"b2\": {\"y\": 3632.0, \"x\": 3911.98117576612}, \"b1\": {\"y\": 3632.0, \"x\": 4028.194352729836}}, \"241\": {\"to_node_id\": \"1576679\", \"from_node_id\": \"1577014\", \"b2\": {\"y\": 3632.0, \"x\": 4036.853918777118}, \"b1\": {\"y\": 3632.0, \"x\": 4065.6561756331353}}, \"239\": {\"to_node_id\": \"1576678\", \"from_node_id\": \"1577014\", \"b2\": {\"y\": 3632.0, \"x\": 4000.5855310681522}, \"b1\": {\"y\": 3632.0, \"x\": 4054.775659320446}}, \"234\": {\"to_node_id\": \"1577014\", \"from_node_id\": \"1577015\", \"b2\": null, \"b1\": null}, \"235\": {\"to_node_id\": \"1577016\", \"from_node_id\": \"1576675\", \"b2\": {\"y\": 3632.0, \"x\": 4133.6977705423415}, \"b1\": {\"y\": 3632.0, \"x\": 4170.325901807804}}, \"236\": {\"to_node_id\": \"1577016\", \"from_node_id\": \"1576554\", \"b2\": {\"y\": 3632.0, \"x\": 4137.5}, \"b1\": {\"y\": 3632.0, \"x\": 4183.0}}, \"237\": {\"to_node_id\": \"1577016\", \"from_node_id\": \"1576676\", \"b2\": {\"y\": 3632.0, \"x\": 4129.669190203266}, \"b1\": {\"y\": 3632.0, \"x\": 4156.897300677553}}, \"233\": {\"to_node_id\": \"1577015\", \"from_node_id\": \"1577016\", \"b2\": null, \"b1\": null}}, \"genes\": [{\"bigg_id\": \"b1761\", \"name\": \"gdhA\"}], \"reversibility\": true, \"metabolites\": [{\"coefficient\": -1.0, \"bigg_id\": \"glu__L_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"nadp_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"nh4_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"h_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"nadph_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"akg_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"h2o_c\"}], \"label_x\": 4025.81298828125, \"label_y\": 3671.95703125, \"gene_reaction_rule\": \"b1761\"}, \"1576773\": {\"name\": \"ATP synthase (four protons for one ATP)\", \"bigg_id\": \"ATPS4r\", \"segments\": {\"245\": {\"to_node_id\": \"1577017\", \"from_node_id\": \"1576681\", \"b2\": {\"y\": 1150.0, \"x\": 4746.487950057818}, \"b1\": {\"y\": 1332.100830078125, \"x\": 4877.134963734394}}, \"244\": {\"to_node_id\": \"1577017\", \"from_node_id\": \"1576680\", \"b2\": {\"y\": 1150.0, \"x\": 4729.0}, \"b1\": {\"y\": 1150.0, \"x\": 4785.0}}, \"247\": {\"to_node_id\": \"1576682\", \"from_node_id\": \"1577018\", \"b2\": {\"y\": 1150.0, \"x\": 4520.0}, \"b1\": {\"y\": 1148.3885498046875, \"x\": 4558.046875}}, \"246\": {\"to_node_id\": \"1577017\", \"from_node_id\": \"1576536\", \"b2\": {\"y\": 1150.0, \"x\": 4796.204440681362}, \"b1\": {\"y\": 1523.8707275390625, \"x\": 4965.504060083706}}, \"243\": {\"to_node_id\": \"1577018\", \"from_node_id\": \"1577019\", \"b2\": null, \"b1\": null}, \"242\": {\"to_node_id\": \"1577019\", \"from_node_id\": \"1577017\", \"b2\": null, \"b1\": null}, \"249\": {\"to_node_id\": \"1576534\", \"from_node_id\": \"1577018\", \"b2\": {\"y\": 1043.64013671875, \"x\": 4324.116846450839}, \"b1\": {\"y\": 1148.3885498046875, \"x\": 4366.332368388376}}, \"248\": {\"to_node_id\": \"1576683\", \"from_node_id\": \"1577018\", \"b2\": {\"y\": 1150.0, \"x\": 4542.7265625}, \"b1\": {\"y\": 1148.388427734375, \"x\": 4575.66162109375}}}, \"genes\": [{\"bigg_id\": \"b3731\", \"name\": \"atpC\"}, {\"bigg_id\": \"b3738\", \"name\": \"atpB\"}, {\"bigg_id\": \"b3733\", \"name\": \"atpG\"}, {\"bigg_id\": \"b3739\", \"name\": \"atpI\"}, {\"bigg_id\": \"b3734\", \"name\": \"atpA\"}, {\"bigg_id\": \"b3736\", \"name\": \"atpF\"}, {\"bigg_id\": \"b3737\", \"name\": \"atpE\"}, {\"bigg_id\": \"b3735\", \"name\": \"atpH\"}, {\"bigg_id\": \"b3732\", \"name\": \"atpD\"}], \"reversibility\": true, \"metabolites\": [{\"coefficient\": -4.0, \"bigg_id\": \"h_e\"}, {\"coefficient\": 1.0, \"bigg_id\": \"atp_c\"}, {\"coefficient\": 3.0, \"bigg_id\": \"h_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"adp_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"pi_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"h2o_c\"}], \"label_x\": 4558.97119140625, \"label_y\": 1122.273193359375, \"gene_reaction_rule\": \"((b3736 and b3737 and b3738) and (b3731 and b3732 and b3733 and b3734 and b3735)) or ((b3736 and b3737 and b3738) and (b3731 and b3732 and b3733 and b3734 and b3735) and b3739)\"}, \"1576770\": {\"name\": \"Fumarate exchange\", \"bigg_id\": \"EX_fum_e\", \"segments\": {\"225\": {\"to_node_id\": \"1577009\", \"from_node_id\": \"1576657\", \"b2\": {\"y\": 560.0, \"x\": 2840.0}, \"b1\": {\"y\": 623.0, \"x\": 2840.0}}}, \"genes\": [], \"reversibility\": false, \"metabolites\": [{\"coefficient\": -1.0, \"bigg_id\": \"fum_e\"}], \"label_x\": 2915.057373046875, \"label_y\": 529.8175659179688, \"gene_reaction_rule\": \"\"}, \"1576771\": {\"name\": \"Succinate transport via proton symport 2 H \", \"bigg_id\": \"SUCCt2_2\", \"segments\": {\"229\": {\"to_node_id\": \"1577013\", \"from_node_id\": \"1576673\", \"b2\": {\"y\": 3027.0, \"x\": 4806.816653826392}, \"b1\": {\"y\": 3027.0, \"x\": 4832.0555127546395}}, \"228\": {\"to_node_id\": \"1577012\", \"from_node_id\": \"1577011\", \"b2\": null, \"b1\": null}, \"227\": {\"to_node_id\": \"1577011\", \"from_node_id\": \"1577013\", \"b2\": null, \"b1\": null}, \"230\": {\"to_node_id\": \"1577013\", \"from_node_id\": \"1576531\", \"b2\": {\"y\": 3027.0, \"x\": 4826.039349194016}, \"b1\": {\"y\": 3027.0, \"x\": 4896.1311639800515}}, \"231\": {\"to_node_id\": \"1576533\", \"from_node_id\": \"1577012\", \"b2\": {\"y\": 3027.0, \"x\": 3972.294403079054}, \"b1\": {\"y\": 3027.0, \"x\": 4366.8883209237165}}, \"232\": {\"to_node_id\": \"1576674\", \"from_node_id\": \"1577012\", \"b2\": {\"y\": 3027.0, \"x\": 4486.0}, \"b1\": {\"y\": 3027.0, \"x\": 4521.0}}}, \"genes\": [{\"bigg_id\": \"b3528\", \"name\": \"dctA\"}], \"reversibility\": false, \"metabolites\": [{\"coefficient\": 1.0, \"bigg_id\": \"succ_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"succ_e\"}, {\"coefficient\": 2.0, \"bigg_id\": \"h_c\"}, {\"coefficient\": -2.0, \"bigg_id\": \"h_e\"}], \"label_x\": 4501.95703125, \"label_y\": 2997.661865234375, \"gene_reaction_rule\": \"b3528\"}, \"1576708\": {\"name\": \"L-Glutamine exchange\", \"bigg_id\": \"EX_gln__L_e\", \"segments\": {\"365\": {\"to_node_id\": \"1576868\", \"from_node_id\": \"1576537\", \"b2\": {\"y\": 4158.0, \"x\": 4951.0}, \"b1\": {\"y\": 4158.0, \"x\": 4902.7}}}, \"genes\": [], \"reversibility\": false, \"metabolites\": [{\"coefficient\": -1.0, \"bigg_id\": \"gln__L_e\"}], \"label_x\": 4933.3095703125, \"label_y\": 4131.884765625, \"gene_reaction_rule\": \"\"}, \"1576703\": {\"name\": \"Ammonia reversible transport\", \"bigg_id\": \"NH4t\", \"segments\": {\"347\": {\"to_node_id\": \"1576858\", \"from_node_id\": \"1576526\", \"b2\": {\"y\": 4677.5, \"x\": 3989.0}, \"b1\": {\"y\": 4733.15, \"x\": 3989.0}}, \"348\": {\"to_node_id\": \"1576527\", \"from_node_id\": \"1576858\", \"b2\": {\"y\": 4519.5, \"x\": 3989.0}, \"b1\": {\"y\": 4574.45, \"x\": 3989.0}}}, \"genes\": [{\"bigg_id\": \"b0451\", \"name\": \"amtB\"}, {\"bigg_id\": \"s0001\", \"name\": \"None\"}], \"reversibility\": true, \"metabolites\": [{\"coefficient\": -1.0, \"bigg_id\": \"nh4_e\"}, {\"coefficient\": 1.0, \"bigg_id\": \"nh4_c\"}], \"label_x\": 3999.0, \"label_y\": 4588.0, \"gene_reaction_rule\": \"s0001 or b0451\"}, \"1576702\": {\"name\": \"Glucose-6-phosphate isomerase\", \"bigg_id\": \"PGI\", \"segments\": {\"345\": {\"to_node_id\": \"1576857\", \"from_node_id\": \"1576524\", \"b2\": {\"y\": 1330.0, \"x\": 1055.0}, \"b1\": {\"y\": 1284.5, \"x\": 1055.0}}, \"346\": {\"to_node_id\": \"1576525\", \"from_node_id\": \"1576857\", \"b2\": {\"y\": 1470.0, \"x\": 1055.0}, \"b1\": {\"y\": 1417.5, \"x\": 1055.0}}}, \"genes\": [{\"bigg_id\": \"b4025\", \"name\": \"pgi\"}], \"reversibility\": true, \"metabolites\": [{\"coefficient\": -1.0, \"bigg_id\": \"g6p_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"f6p_c\"}], \"label_x\": 1065.0, \"label_y\": 1385.0, \"gene_reaction_rule\": \"b4025\"}, \"1576701\": {\"name\": \"Enolase\", \"bigg_id\": \"ENO\", \"segments\": {\"344\": {\"to_node_id\": \"1576523\", \"from_node_id\": \"1576855\", \"b2\": {\"y\": 3319.0832691319597, \"x\": 1055.0}, \"b1\": {\"y\": 3281.224980739588, \"x\": 1055.0}}, \"341\": {\"to_node_id\": \"1576856\", \"from_node_id\": \"1576522\", \"b2\": {\"y\": 3175.0, \"x\": 1055.0}, \"b1\": {\"y\": 3173.322021484375, \"x\": 1055.0}}, \"342\": {\"to_node_id\": \"1576855\", \"from_node_id\": \"1576856\", \"b2\": null, \"b1\": null}, \"343\": {\"to_node_id\": \"1576517\", \"from_node_id\": \"1576855\", \"b2\": {\"y\": 3320.0, \"x\": 1055.0}, \"b1\": {\"y\": 3281.5, \"x\": 1055.0}}}, \"genes\": [{\"bigg_id\": \"b2779\", \"name\": \"eno\"}], \"reversibility\": true, \"metabolites\": [{\"coefficient\": 1.0, \"bigg_id\": \"pep_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"h2o_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"2pg_c\"}], \"label_x\": 1065.0, \"label_y\": 3215.0, \"gene_reaction_rule\": \"b2779\"}, \"1576700\": {\"name\": \"Phosphoenolpyruvate carboxylase\", \"bigg_id\": \"PPC\", \"segments\": {\"339\": {\"to_node_id\": \"1576520\", \"from_node_id\": \"1576853\", \"b2\": {\"y\": 3552.157197214914, \"x\": 1572.3763206054002}, \"b1\": {\"y\": 3528.947159164474, \"x\": 1516.81289618162}}, \"338\": {\"to_node_id\": \"1576519\", \"from_node_id\": \"1576853\", \"b2\": {\"y\": 3543.994528732819, \"x\": 1552.8353869664456}, \"b1\": {\"y\": 3526.498358619846, \"x\": 1510.9506160899336}}, \"335\": {\"to_node_id\": \"1576854\", \"from_node_id\": \"1576516\", \"b2\": {\"y\": 3452.2308899862983, \"x\": 1334.4013835391315}, \"b1\": {\"y\": 3438.769633287661, \"x\": 1302.6712784637714}}, \"334\": {\"to_node_id\": \"1576853\", \"from_node_id\": \"1576852\", \"b2\": null, \"b1\": null}, \"337\": {\"to_node_id\": \"1576854\", \"from_node_id\": \"1576518\", \"b2\": {\"y\": 3447.322871456709, \"x\": 1322.8324827193853}, \"b1\": {\"y\": 3422.409571522363, \"x\": 1264.1082757312843}}, \"336\": {\"to_node_id\": \"1576854\", \"from_node_id\": \"1576517\", \"b2\": {\"y\": 3440.1599283734054, \"x\": 1305.9484025944557}, \"b1\": {\"y\": 3398.5330945780183, \"x\": 1207.828008648186}}, \"333\": {\"to_node_id\": \"1576852\", \"from_node_id\": \"1576854\", \"b2\": null, \"b1\": null}, \"340\": {\"to_node_id\": \"1576521\", \"from_node_id\": \"1576853\", \"b2\": {\"y\": 3691.624192938869, \"x\": 2580.289220436279}, \"b1\": {\"y\": 3708.0419942097856, \"x\": 1966.279649431665}}}, \"genes\": [{\"bigg_id\": \"b3956\", \"name\": \"ppc\"}], \"reversibility\": false, \"metabolites\": [{\"coefficient\": -1.0, \"bigg_id\": \"h2o_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"oaa_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"h_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"pep_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"pi_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"co2_c\"}], \"label_x\": 1490.0008544921875, \"label_y\": 3479.666748046875, \"gene_reaction_rule\": \"b3956\"}, \"1576707\": {\"name\": \"Adenylate kinase\", \"bigg_id\": \"ADK1\", \"segments\": {\"364\": {\"to_node_id\": \"1576867\", \"from_node_id\": \"1576535\", \"b2\": {\"y\": 1177.1034937162894, \"x\": 4145.0}, \"b1\": {\"y\": 1123.6783123876312, \"x\": 4145.0}}, \"362\": {\"to_node_id\": \"1576536\", \"from_node_id\": \"1576866\", \"b2\": {\"y\": 1441.4029541015625, \"x\": 4146.15869140625}, \"b1\": {\"y\": 1352.2913818359375, \"x\": 4145.83056640625}}, \"363\": {\"to_node_id\": \"1576867\", \"from_node_id\": \"1576534\", \"b2\": {\"y\": 1173.8036262051405, \"x\": 4145.0}, \"b1\": {\"y\": 1112.6787540171351, \"x\": 4145.0}}, \"361\": {\"to_node_id\": \"1576866\", \"from_node_id\": \"1576867\", \"b2\": null, \"b1\": null}}, \"genes\": [{\"bigg_id\": \"b0474\", \"name\": \"adk\"}], \"reversibility\": true, \"metabolites\": [{\"coefficient\": -1.0, \"bigg_id\": \"amp_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"atp_c\"}, {\"coefficient\": 2.0, \"bigg_id\": \"adp_c\"}], \"label_x\": 3997.07177734375, \"label_y\": 1251.6114501953125, \"gene_reaction_rule\": \"b0474\"}, \"1576706\": {\"name\": \"Isocitrate lyase\", \"bigg_id\": \"ICL\", \"segments\": {\"357\": {\"to_node_id\": \"1576865\", \"from_node_id\": \"1576505\", \"b2\": {\"y\": 3916.5, \"x\": 3415.0}, \"b1\": {\"y\": 3958.15, \"x\": 3413.6}}, \"360\": {\"to_node_id\": \"1576533\", \"from_node_id\": \"1576864\", \"b2\": {\"y\": 3316.4974972260998, \"x\": 3417.0}, \"b1\": {\"y\": 3631.14924916783, \"x\": 3417.0}}, \"359\": {\"to_node_id\": \"1576532\", \"from_node_id\": \"1576864\", \"b2\": {\"y\": 3612.2266277927156, \"x\": 3417.0}, \"b1\": {\"y\": 3719.8679883378145, \"x\": 3417.0}}, \"358\": {\"to_node_id\": \"1576864\", \"from_node_id\": \"1576865\", \"b2\": null, \"b1\": null}}, \"genes\": [{\"bigg_id\": \"b4015\", \"name\": \"aceA\"}], \"reversibility\": false, \"metabolites\": [{\"coefficient\": 1.0, \"bigg_id\": \"succ_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"glx_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"icit_c\"}], \"label_x\": 3427.0, \"label_y\": 3847.0, \"gene_reaction_rule\": \"b4015\"}, \"1576705\": {\"name\": \"Succinate exchange\", \"bigg_id\": \"EX_succ_e\", \"segments\": {\"355\": {\"to_node_id\": \"1576863\", \"from_node_id\": \"1576531\", \"b2\": {\"y\": 2880.0, \"x\": 4978.5}, \"b1\": {\"y\": 2880.0, \"x\": 4945.95}}}, \"genes\": [], \"reversibility\": false, \"metabolites\": [{\"coefficient\": -1.0, \"bigg_id\": \"succ_e\"}], \"label_x\": 4968.927734375, \"label_y\": 2845.8271484375, \"gene_reaction_rule\": \"\"}, \"1576704\": {\"name\": \"Fructose-bisphosphatase\", \"bigg_id\": \"FBP\", \"segments\": {\"354\": {\"to_node_id\": \"1576530\", \"from_node_id\": \"1576860\", \"b2\": {\"y\": 1600.9687576256715, \"x\": 835.0}, \"b1\": {\"y\": 1645.7906272877015, \"x\": 835.0}}, \"353\": {\"to_node_id\": \"1576525\", \"from_node_id\": \"1576860\", \"b2\": {\"y\": 1539.7003591385833, \"x\": 835.0}, \"b1\": {\"y\": 1627.410107741575, \"x\": 835.0}}, \"352\": {\"to_node_id\": \"1576861\", \"from_node_id\": \"1576529\", \"b2\": {\"y\": 1805.8044115262064, \"x\": 835.0}, \"b1\": {\"y\": 1901.0147050873545, \"x\": 835.0}}, \"351\": {\"to_node_id\": \"1576861\", \"from_node_id\": \"1576528\", \"b2\": {\"y\": 1782.1026313764871, \"x\": 835.0}, \"b1\": {\"y\": 1822.0087712549569, \"x\": 835.0}}, \"350\": {\"to_node_id\": \"1576860\", \"from_node_id\": \"1576859\", \"b2\": null, \"b1\": null}, \"349\": {\"to_node_id\": \"1576859\", \"from_node_id\": \"1576861\", \"b2\": null, \"b1\": null}}, \"genes\": [{\"bigg_id\": \"b4232\", \"name\": \"fbp\"}, {\"bigg_id\": \"b3925\", \"name\": \"glpX\"}], \"reversibility\": false, \"metabolites\": [{\"coefficient\": -1.0, \"bigg_id\": \"fdp_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"h2o_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"pi_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"f6p_c\"}], \"label_x\": 751.3809814453125, \"label_y\": 1733.561767578125, \"gene_reaction_rule\": \"b3925 or b4232\"}, \"1577548\": {\"name\": \"Aconitase (half-reaction A, Citrate hydro-lyase)\", \"bigg_id\": \"ACONTa\", \"segments\": {\"511\": {\"to_node_id\": \"1576685\", \"from_node_id\": \"1577099\", \"b2\": {\"y\": 4010.3555557486548, \"x\": 3025.1432055426353}, \"b1\": {\"y\": 4012.0956406322803, \"x\": 3025.7810749583687}}, \"507\": {\"to_node_id\": \"1577100\", \"from_node_id\": \"1577099\", \"b2\": null, \"b1\": null}, \"510\": {\"to_node_id\": \"1577103\", \"from_node_id\": \"1577101\", \"b2\": {\"y\": 4045.94073519232, \"x\": 3126.6630443215304}, \"b1\": {\"y\": 4044.2013827305695, \"x\": 3108.646756937047}}, \"508\": {\"to_node_id\": \"1577100\", \"from_node_id\": \"1577101\", \"b2\": null, \"b1\": null}, \"509\": {\"to_node_id\": \"1577102\", \"from_node_id\": \"1577101\", \"b2\": {\"y\": 3993.9154608910203, \"x\": 3112.4765239553985}, \"b1\": {\"y\": 4044.558477078102, \"x\": 3125.4142208172407}}}, \"genes\": [{\"bigg_id\": \"b0118\", \"name\": \"acnB\"}, {\"bigg_id\": \"b1276\", \"name\": \"acnA\"}], \"reversibility\": true, \"metabolites\": [{\"coefficient\": -1.0, \"bigg_id\": \"cit_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"h2o_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"acon_C_c\"}], \"label_x\": 2982.991259697708, \"label_y\": 4088.6436288689247, \"gene_reaction_rule\": \"b0118 or b1276\"}, \"1577549\": {\"name\": \"Aconitase (half-reaction B, Isocitrate hydro-lyase)\", \"bigg_id\": \"ACONTb\", \"segments\": {\"513\": {\"to_node_id\": \"1577105\", \"from_node_id\": \"1577106\", \"b2\": null, \"b1\": null}, \"512\": {\"to_node_id\": \"1577105\", \"from_node_id\": \"1577104\", \"b2\": null, \"b1\": null}, \"515\": {\"to_node_id\": \"1576505\", \"from_node_id\": \"1577106\", \"b2\": {\"y\": 4013.1918298584474, \"x\": 3345.4226198631072}, \"b1\": {\"y\": 4014.8024620364417, \"x\": 3344.105236291347}}, \"514\": {\"to_node_id\": \"1577102\", \"from_node_id\": \"1577104\", \"b2\": {\"y\": 3991.5491179659602, \"x\": 3266.0226278968735}, \"b1\": {\"y\": 4044.360816430351, \"x\": 3261.354277099013}}, \"516\": {\"to_node_id\": \"1577103\", \"from_node_id\": \"1577104\", \"b2\": {\"y\": 4046.222560627323, \"x\": 3260.813883986344}, \"b1\": {\"y\": 4046.056264386829, \"x\": 3262.862957011229}}}, \"genes\": [{\"bigg_id\": \"b1276\", \"name\": \"acnA\"}, {\"bigg_id\": \"b0118\", \"name\": \"acnB\"}], \"reversibility\": true, \"metabolites\": [{\"coefficient\": 1.0, \"bigg_id\": \"icit_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"h2o_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"acon_C_c\"}], \"label_x\": 3291.833217745038, \"label_y\": 4081.35343547726, \"gene_reaction_rule\": \"b0118 or b1276\"}, \"1577546\": {\"name\": \"D lactate transport via proton symport\", \"bigg_id\": \"D_LACt2\", \"segments\": {\"476\": {\"to_node_id\": \"1577068\", \"from_node_id\": \"1577067\", \"b2\": null, \"b1\": null}, \"477\": {\"to_node_id\": \"1577068\", \"from_node_id\": \"1577069\", \"b2\": null, \"b1\": null}, \"478\": {\"to_node_id\": \"1576556\", \"from_node_id\": \"1577067\", \"b2\": {\"y\": 4736.89501953125, \"x\": 1054.9998779296875}, \"b1\": {\"y\": 4692.98193359375, \"x\": 1055.0}}, \"479\": {\"to_node_id\": \"1577071\", \"from_node_id\": \"1577067\", \"b2\": {\"y\": 4715.98193359375, \"x\": 1076.0}, \"b1\": {\"y\": 4696.98193359375, \"x\": 1055.0}}, \"481\": {\"to_node_id\": \"1577072\", \"from_node_id\": \"1577069\", \"b2\": {\"y\": 4451.7353515625, \"x\": 1074.4132080078125}, \"b1\": {\"y\": 4470.7353515625, \"x\": 1053.4132080078125}}, \"480\": {\"to_node_id\": \"1576584\", \"from_node_id\": \"1577069\", \"b2\": {\"y\": 4413.36767578125, \"x\": 1055.0}, \"b1\": {\"y\": 4474.7353515625, \"x\": 1053.4132080078125}}}, \"genes\": [{\"bigg_id\": \"b3603\", \"name\": \"lldP\"}, {\"bigg_id\": \"b2975\", \"name\": \"glcA\"}], \"reversibility\": true, \"metabolites\": [{\"coefficient\": -1.0, \"bigg_id\": \"lac__D_e\"}, {\"coefficient\": 1.0, \"bigg_id\": \"h_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"lac__D_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"h_e\"}], \"label_x\": 1078.65283203125, \"label_y\": 4609.72607421875, \"gene_reaction_rule\": \"b2975 or b3603\"}, \"1576711\": {\"name\": \"Transaldolase\", \"bigg_id\": \"TALA\", \"segments\": {\"369\": {\"to_node_id\": \"1576872\", \"from_node_id\": \"1576873\", \"b2\": null, \"b1\": null}, \"371\": {\"to_node_id\": \"1576873\", \"from_node_id\": \"1576545\", \"b2\": {\"y\": 1856.1727017464573, \"x\": 2173.815906732547}, \"b1\": {\"y\": 1892.1518443631908, \"x\": 2283.184776999114}}, \"370\": {\"to_node_id\": \"1576874\", \"from_node_id\": \"1576872\", \"b2\": null, \"b1\": null}, \"373\": {\"to_node_id\": \"1576547\", \"from_node_id\": \"1576874\", \"b2\": {\"y\": 2171.7496092324754, \"x\": 2178.0}, \"b1\": {\"y\": 2076.976957965055, \"x\": 2178.0}}, \"372\": {\"to_node_id\": \"1576873\", \"from_node_id\": \"1576546\", \"b2\": {\"y\": 1856.1728238167698, \"x\": 2175.402576654422}, \"b1\": {\"y\": 1890.5651744413158, \"x\": 2064.2112662569266}}, \"374\": {\"to_node_id\": \"1576548\", \"from_node_id\": \"1576874\", \"b2\": {\"y\": 2170.2455578331073, \"x\": 2178.000244140625}, \"b1\": {\"y\": 2077.4778177405574, \"x\": 2178.0}}}, \"genes\": [{\"bigg_id\": \"b2464\", \"name\": \"talA\"}, {\"bigg_id\": \"b0008\", \"name\": \"talB\"}], \"reversibility\": true, \"metabolites\": [{\"coefficient\": -1.0, \"bigg_id\": \"s7p_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"g3p_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"f6p_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"e4p_c\"}], \"label_x\": 2197.5205078125, \"label_y\": 2008.8013916015625, \"gene_reaction_rule\": \"b2464 or b0008\"}, \"1576712\": {\"name\": \"Pyruvate kinase\", \"bigg_id\": \"PYK\", \"segments\": {\"380\": {\"to_node_id\": \"1576511\", \"from_node_id\": \"1576877\", \"b2\": {\"y\": 3845.0, \"x\": 1055.0}, \"b1\": {\"y\": 3775.0, \"x\": 1055.0}}, \"381\": {\"to_node_id\": \"1576551\", \"from_node_id\": \"1576877\", \"b2\": {\"y\": 3802.008771254957, \"x\": 1055.0}, \"b1\": {\"y\": 3762.102631376487, \"x\": 1055.0}}, \"379\": {\"to_node_id\": \"1576875\", \"from_node_id\": \"1576517\", \"b2\": {\"y\": 3613.0, \"x\": 1055.0}, \"b1\": {\"y\": 3515.0, \"x\": 1055.0}}, \"378\": {\"to_node_id\": \"1576875\", \"from_node_id\": \"1576550\", \"b2\": {\"y\": 3634.8195639293895, \"x\": 1055.0}, \"b1\": {\"y\": 3587.7318797646312, \"x\": 1055.0}}, \"375\": {\"to_node_id\": \"1576876\", \"from_node_id\": \"1576875\", \"b2\": null, \"b1\": null}, \"377\": {\"to_node_id\": \"1576875\", \"from_node_id\": \"1576549\", \"b2\": {\"y\": 3640.769750529242, \"x\": 1055.0}, \"b1\": {\"y\": 3607.5658350974745, \"x\": 1055.0}}, \"376\": {\"to_node_id\": \"1576877\", \"from_node_id\": \"1576876\", \"b2\": null, \"b1\": null}}, \"genes\": [{\"bigg_id\": \"b1854\", \"name\": \"pykA\"}, {\"bigg_id\": \"b1676\", \"name\": \"pykF\"}], \"reversibility\": false, \"metabolites\": [{\"coefficient\": -1.0, \"bigg_id\": \"pep_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"adp_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"pyr_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"atp_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"h_c\"}], \"label_x\": 1065.0, \"label_y\": 3695.0, \"gene_reaction_rule\": \"b1854 or b1676\"}, \"1576713\": {\"name\": \"L glutamate transport via proton symport reversible\", \"bigg_id\": \"GLUt2r\", \"segments\": {\"382\": {\"to_node_id\": \"1576878\", \"from_node_id\": \"1576880\", \"b2\": null, \"b1\": null}, \"383\": {\"to_node_id\": \"1576879\", \"from_node_id\": \"1576878\", \"b2\": null, \"b1\": null}, \"384\": {\"to_node_id\": \"1576880\", \"from_node_id\": \"1576552\", \"b2\": {\"y\": 3635.0, \"x\": 4794.600721137178}, \"b1\": {\"y\": 3635.0, \"x\": 4831.002403790594}}, \"385\": {\"to_node_id\": \"1576880\", \"from_node_id\": \"1576553\", \"b2\": {\"y\": 3635.0, \"x\": 4791.314625451064}, \"b1\": {\"y\": 3635.0, \"x\": 4820.0487515035475}}, \"386\": {\"to_node_id\": \"1576554\", \"from_node_id\": \"1576879\", \"b2\": {\"y\": 3635.0, \"x\": 4394.492321020364}, \"b1\": {\"y\": 3635.0, \"x\": 4497.047696306109}}, \"387\": {\"to_node_id\": \"1576555\", \"from_node_id\": \"1576879\", \"b2\": {\"y\": 3635.0, \"x\": 4499.835695074495}, \"b1\": {\"y\": 3635.0, \"x\": 4528.650708522348}}}, \"genes\": [{\"bigg_id\": \"b4077\", \"name\": \"gltP\"}], \"reversibility\": true, \"metabolites\": [{\"coefficient\": 1.0, \"bigg_id\": \"glu__L_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"h_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"glu__L_e\"}, {\"coefficient\": -1.0, \"bigg_id\": \"h_e\"}], \"label_x\": 4655.0, \"label_y\": 3625.0, \"gene_reaction_rule\": \"b4077\"}, \"1576714\": {\"name\": \"D-lactate exchange\", \"bigg_id\": \"EX_lac__D_e\", \"segments\": {\"388\": {\"to_node_id\": \"1576881\", \"from_node_id\": \"1576556\", \"b2\": {\"y\": 4880.0, \"x\": 1055.0}, \"b1\": {\"y\": 4861.19384765625, \"x\": 1055.0}}}, \"genes\": [], \"reversibility\": false, \"metabolites\": [{\"coefficient\": -1.0, \"bigg_id\": \"lac__D_e\"}], \"label_x\": 1065.0, \"label_y\": 4915.0, \"gene_reaction_rule\": \"\"}, \"1576715\": {\"name\": \"Pyruvate exchange\", \"bigg_id\": \"EX_pyr_e\", \"segments\": {\"390\": {\"to_node_id\": \"1576884\", \"from_node_id\": \"1576557\", \"b2\": {\"y\": 3945.0, \"x\": 215.0}, \"b1\": {\"y\": 3945.0, \"x\": 257.0}}}, \"genes\": [], \"reversibility\": false, \"metabolites\": [{\"coefficient\": -1.0, \"bigg_id\": \"pyr_e\"}], \"label_x\": 96.76921081542969, \"label_y\": 3904.851318359375, \"gene_reaction_rule\": \"\"}, \"1576716\": {\"name\": \"Ribulose 5-phosphate 3-epimerase\", \"bigg_id\": \"RPE\", \"segments\": {\"393\": {\"to_node_id\": \"1576558\", \"from_node_id\": \"1576885\", \"b2\": {\"y\": 1485.0, \"x\": 1995.0}, \"b1\": {\"y\": 1436.0, \"x\": 2030.0}}, \"392\": {\"to_node_id\": \"1576885\", \"from_node_id\": \"1576493\", \"b2\": {\"y\": 1340.0, \"x\": 2100.0}, \"b1\": {\"y\": 1287.5, \"x\": 2138.5}}}, \"genes\": [{\"bigg_id\": \"b4301\", \"name\": \"sgcE\"}, {\"bigg_id\": \"b3386\", \"name\": \"rpe\"}], \"reversibility\": true, \"metabolites\": [{\"coefficient\": 1.0, \"bigg_id\": \"xu5p__D_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"ru5p__D_c\"}], \"label_x\": 1937.57958984375, \"label_y\": 1422.4544677734375, \"gene_reaction_rule\": \"b3386 or b4301\"}, \"1576717\": {\"name\": \"H+ exchange\", \"bigg_id\": \"EX_h_e\", \"segments\": {\"394\": {\"to_node_id\": \"1576887\", \"from_node_id\": \"1576559\", \"b2\": {\"y\": 2229.5, \"x\": 4918.0}, \"b1\": {\"y\": 2229.15, \"x\": 4870.4}}}, \"genes\": [], \"reversibility\": true, \"metabolites\": [{\"coefficient\": -1.0, \"bigg_id\": \"h_e\"}], \"label_x\": 4920.2587890625, \"label_y\": 2195.827392578125, \"gene_reaction_rule\": \"\"}, \"1576761\": {\"name\": \"Glutamate synthase (NADPH)\", \"bigg_id\": \"GLUSy\", \"segments\": {\"184\": {\"to_node_id\": \"1576554\", \"from_node_id\": \"1576987\", \"b2\": {\"y\": 3721.4298613416263, \"x\": 4116.0}, \"b1\": {\"y\": 3810.0289584024877, \"x\": 4116.0}}, \"182\": {\"to_node_id\": \"1576986\", \"from_node_id\": \"1576655\", \"b2\": {\"y\": 3907.2123007471773, \"x\": 4116.0}, \"b1\": {\"y\": 3952.041002490592, \"x\": 4116.0}}, \"183\": {\"to_node_id\": \"1576656\", \"from_node_id\": \"1576987\", \"b2\": {\"y\": 3790.445677833893, \"x\": 4116.0}, \"b1\": {\"y\": 3830.733703350168, \"x\": 4116.0}}, \"180\": {\"to_node_id\": \"1576986\", \"from_node_id\": \"1576654\", \"b2\": {\"y\": 3902.6240384299276, \"x\": 4116.0}, \"b1\": {\"y\": 3936.7467947664254, \"x\": 4116.0}}, \"181\": {\"to_node_id\": \"1576986\", \"from_node_id\": \"1576578\", \"b2\": {\"y\": 3924.538472874492, \"x\": 4116.0}, \"b1\": {\"y\": 4009.7949095816407, \"x\": 4116.0}}, \"179\": {\"to_node_id\": \"1576986\", \"from_node_id\": \"1576509\", \"b2\": {\"y\": 4155.746259618145, \"x\": 4119.22314453125}, \"b1\": {\"y\": 4145.014875810485, \"x\": 3943.568359375}}, \"178\": {\"to_node_id\": \"1576987\", \"from_node_id\": \"1576988\", \"b2\": null, \"b1\": null}, \"177\": {\"to_node_id\": \"1576988\", \"from_node_id\": \"1576986\", \"b2\": null, \"b1\": null}}, \"genes\": [{\"bigg_id\": \"b3212\", \"name\": \"gltB\"}, {\"bigg_id\": \"b3213\", \"name\": \"gltD\"}], \"reversibility\": false, \"metabolites\": [{\"coefficient\": 2.0, \"bigg_id\": \"glu__L_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"nadp_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"h_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"nadph_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"gln__L_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"akg_c\"}], \"label_x\": 3984.18701171875, \"label_y\": 3851.553955078125, \"gene_reaction_rule\": \"b3212 and b3213\"}, \"1577490\": {\"name\": \"Acetaldehyde dehydrogenase (acetylating)\", \"bigg_id\": \"ACALD\", \"segments\": {\"280\": {\"to_node_id\": \"1576812\", \"from_node_id\": \"1577033\", \"b2\": {\"y\": 3945.0, \"x\": 1918.1029079843004}, \"b1\": {\"y\": 3945.0, \"x\": 1961.4308723952902}}, \"273\": {\"to_node_id\": \"1577032\", \"from_node_id\": \"1577031\", \"b2\": null, \"b1\": null}, \"274\": {\"to_node_id\": \"1577033\", \"from_node_id\": \"1577032\", \"b2\": null, \"b1\": null}, \"275\": {\"to_node_id\": \"1577031\", \"from_node_id\": \"1576817\", \"b2\": {\"y\": 3945.0, \"x\": 2065.5}, \"b1\": {\"y\": 3945.0, \"x\": 2125.0}}, \"276\": {\"to_node_id\": \"1577031\", \"from_node_id\": \"1576825\", \"b2\": {\"y\": 3945.0, \"x\": 2058.56912760471}, \"b1\": {\"y\": 3945.0, \"x\": 2101.8970920157}}, \"277\": {\"to_node_id\": \"1577031\", \"from_node_id\": \"1576828\", \"b2\": {\"y\": 3945.0, \"x\": 2053.52081728299}, \"b1\": {\"y\": 3945.0, \"x\": 2085.0693909433}}, \"278\": {\"to_node_id\": \"1576514\", \"from_node_id\": \"1577033\", \"b2\": {\"y\": 3945.0, \"x\": 1847.5}, \"b1\": {\"y\": 3945.0, \"x\": 1940.25}}, \"279\": {\"to_node_id\": \"1576832\", \"from_node_id\": \"1577033\", \"b2\": {\"y\": 3945.0, \"x\": 1934.9306090567002}, \"b1\": {\"y\": 3945.0, \"x\": 1966.47918271701}}}, \"genes\": [{\"bigg_id\": \"b1241\", \"name\": \"adhE\"}, {\"bigg_id\": \"b0351\", \"name\": \"mhpF\"}], \"reversibility\": true, \"metabolites\": [{\"coefficient\": 1.0, \"bigg_id\": \"nadh_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"coa_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"acald_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"h_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"nad_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"accoa_c\"}], \"label_x\": 1939.0750732421875, \"label_y\": 3998.470458984375, \"gene_reaction_rule\": \"b0351 or b1241\"}, \"1577497\": {\"name\": \"Acetaldehyde reversible transport\", \"bigg_id\": \"ACALDt\", \"segments\": {\"281\": {\"to_node_id\": \"1577034\", \"from_node_id\": \"1576815\", \"b2\": {\"y\": 4707.0, \"x\": 2210.0}, \"b1\": {\"y\": 4796.55283203125, \"x\": 2210.0}}, \"282\": {\"to_node_id\": \"1576817\", \"from_node_id\": \"1577034\", \"b2\": {\"y\": 4257.5, \"x\": 2210.0}, \"b1\": {\"y\": 4476.25, \"x\": 2210.0}}}, \"genes\": [{\"bigg_id\": \"s0001\", \"name\": \"None\"}], \"reversibility\": true, \"metabolites\": [{\"coefficient\": 1.0, \"bigg_id\": \"acald_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"acald_e\"}], \"label_x\": 2248.561767578125, \"label_y\": 4579.041015625, \"gene_reaction_rule\": \"s0001\"}, \"1577498\": {\"name\": \"Acetaldehyde exchange\", \"bigg_id\": \"EX_acald_e\", \"segments\": {\"283\": {\"to_node_id\": \"1577035\", \"from_node_id\": \"1576815\", \"b2\": {\"y\": 4894.0, \"x\": 2210.5}, \"b1\": {\"y\": 4889.1484375, \"x\": 2210.15}}}, \"genes\": [], \"reversibility\": false, \"metabolites\": [{\"coefficient\": -1.0, \"bigg_id\": \"acald_e\"}], \"label_x\": 2227.346923828125, \"label_y\": 4922.892578125, \"gene_reaction_rule\": \"\"}, \"1576724\": {\"name\": \"Glutamine synthetase\", \"bigg_id\": \"GLNS\", \"segments\": {\"424\": {\"to_node_id\": \"1576578\", \"from_node_id\": \"1576899\", \"b2\": {\"y\": 4007.2677048056653, \"x\": 4364.0}, \"b1\": {\"y\": 3924.4803114416995, \"x\": 4364.0}}, \"421\": {\"to_node_id\": \"1576901\", \"from_node_id\": \"1576554\", \"b2\": {\"y\": 3812.091159595566, \"x\": 4364.0}, \"b1\": {\"y\": 3725.970531985219, \"x\": 4364.0}}, \"420\": {\"to_node_id\": \"1576899\", \"from_node_id\": \"1576900\", \"b2\": null, \"b1\": null}, \"423\": {\"to_node_id\": \"1576901\", \"from_node_id\": \"1576577\", \"b2\": {\"y\": 3835.8273009599397, \"x\": 4364.0}, \"b1\": {\"y\": 3805.0910031997996, \"x\": 4364.0}}, \"422\": {\"to_node_id\": \"1576901\", \"from_node_id\": \"1576576\", \"b2\": {\"y\": 3830.8679841164862, \"x\": 4364.0}, \"b1\": {\"y\": 3788.5599470549537, \"x\": 4364.0}}, \"425\": {\"to_node_id\": \"1576579\", \"from_node_id\": \"1576899\", \"b2\": {\"y\": 3936.762432936357, \"x\": 4364.0}, \"b1\": {\"y\": 3903.3287298809073, \"x\": 4364.0}}, \"419\": {\"to_node_id\": \"1576900\", \"from_node_id\": \"1576901\", \"b2\": null, \"b1\": null}, \"427\": {\"to_node_id\": \"1576581\", \"from_node_id\": \"1576899\", \"b2\": {\"y\": 3978.617241644675, \"x\": 4364.0}, \"b1\": {\"y\": 3915.8851724934025, \"x\": 4364.0}}, \"426\": {\"to_node_id\": \"1576580\", \"from_node_id\": \"1576899\", \"b2\": {\"y\": 3956.314560089181, \"x\": 4364.0}, \"b1\": {\"y\": 3909.1943680267545, \"x\": 4364.0}}}, \"genes\": [{\"bigg_id\": \"b3870\", \"name\": \"glnA\"}, {\"bigg_id\": \"b1297\", \"name\": \"puuA\"}], \"reversibility\": false, \"metabolites\": [{\"coefficient\": -1.0, \"bigg_id\": \"glu__L_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"atp_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"nh4_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"h_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"adp_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"pi_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"gln__L_c\"}], \"label_x\": 4377.22314453125, \"label_y\": 3875.114990234375, \"gene_reaction_rule\": \"b3870 or b1297\"}, \"1576727\": {\"name\": \"2-Oxogluterate dehydrogenase\", \"bigg_id\": \"AKGDH\", \"segments\": {\"443\": {\"to_node_id\": \"1576908\", \"from_node_id\": \"1576585\", \"b2\": {\"y\": 3494.907757737772, \"x\": 3779.4266247023934}, \"b1\": {\"y\": 3529.6925257925727, \"x\": 3778.088749007978}}, \"442\": {\"to_node_id\": \"1576908\", \"from_node_id\": \"1576509\", \"b2\": {\"y\": 3502.6153909632244, \"x\": 3779.130177270645}, \"b1\": {\"y\": 3555.384636544082, \"x\": 3777.1005909021505}}, \"441\": {\"to_node_id\": \"1576910\", \"from_node_id\": \"1576909\", \"b2\": null, \"b1\": null}, \"440\": {\"to_node_id\": \"1576909\", \"from_node_id\": \"1576908\", \"b2\": null, \"b1\": null}, \"447\": {\"to_node_id\": \"1576588\", \"from_node_id\": \"1576910\", \"b2\": {\"y\": 3276.99346892041, \"x\": 3770.846587256151}, \"b1\": {\"y\": 3314.098040676123, \"x\": 3774.453976176845}}, \"446\": {\"to_node_id\": \"1576587\", \"from_node_id\": \"1576910\", \"b2\": {\"y\": 3277.236984619066, \"x\": 3770.8702623935205}, \"b1\": {\"y\": 3314.1710953857196, \"x\": 3774.4610787180563}}, \"445\": {\"to_node_id\": \"1576570\", \"from_node_id\": \"1576910\", \"b2\": {\"y\": 3259.378766045427, \"x\": 3769.134046698861}, \"b1\": {\"y\": 3308.813629813628, \"x\": 3773.940214009658}}, \"444\": {\"to_node_id\": \"1576908\", \"from_node_id\": \"1576586\", \"b2\": {\"y\": 3498.5402671676266, \"x\": 3779.2869128012453}, \"b1\": {\"y\": 3541.8008905587553, \"x\": 3777.623042670817}}}, \"genes\": [{\"bigg_id\": \"b0116\", \"name\": \"lpd\"}, {\"bigg_id\": \"b0726\", \"name\": \"sucA\"}, {\"bigg_id\": \"b0727\", \"name\": \"sucB\"}], \"reversibility\": false, \"metabolites\": [{\"coefficient\": 1.0, \"bigg_id\": \"nadh_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"coa_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"nad_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"co2_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"succoa_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"akg_c\"}], \"label_x\": 3793.0, \"label_y\": 3392.0, \"gene_reaction_rule\": \"b0116 and b0726 and b0727\"}, \"1576726\": {\"name\": \"NAD transhydrogenase\", \"bigg_id\": \"NADTRHD\", \"segments\": {\"438\": {\"to_node_id\": \"1576496\", \"from_node_id\": \"1576905\", \"b2\": {\"y\": 1270.0, \"x\": 3400.0}, \"b1\": {\"y\": 1270.0, \"x\": 3449.0}}, \"439\": {\"to_node_id\": \"1576497\", \"from_node_id\": \"1576905\", \"b2\": {\"y\": 1270.0, \"x\": 3418.3388104489154}, \"b1\": {\"y\": 1270.0, \"x\": 3446.2829175487373}}, \"436\": {\"to_node_id\": \"1576906\", \"from_node_id\": \"1576501\", \"b2\": {\"y\": 1270.0, \"x\": 3592.9455878111676}, \"b1\": {\"y\": 1270.0001220703125, \"x\": 3627.146913797642}}, \"437\": {\"to_node_id\": \"1576906\", \"from_node_id\": \"1576500\", \"b2\": {\"y\": 1270.0, \"x\": 3595.5}, \"b1\": {\"y\": 1270.0, \"x\": 3655.0}}, \"434\": {\"to_node_id\": \"1576907\", \"from_node_id\": \"1576906\", \"b2\": null, \"b1\": null}, \"435\": {\"to_node_id\": \"1576905\", \"from_node_id\": \"1576907\", \"b2\": null, \"b1\": null}}, \"genes\": [{\"bigg_id\": \"b1602\", \"name\": \"pntB\"}, {\"bigg_id\": \"b3962\", \"name\": \"sthA\"}, {\"bigg_id\": \"b1603\", \"name\": \"pntA\"}], \"reversibility\": false, \"metabolites\": [{\"coefficient\": 1.0, \"bigg_id\": \"nadh_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"nadp_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"nad_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"nadph_c\"}], \"label_x\": 3478.431396484375, \"label_y\": 1319.6259765625, \"gene_reaction_rule\": \"b3962 or (b1602 and b1603)\"}, \"1576721\": {\"name\": \"Phosphofructokinase\", \"bigg_id\": \"PFK\", \"segments\": {\"407\": {\"to_node_id\": \"1576893\", \"from_node_id\": \"1576572\", \"b2\": {\"y\": 1674.9065411518764, \"x\": 1055.0}, \"b1\": {\"y\": 1639.6884705062548, \"x\": 1055.0}}, \"406\": {\"to_node_id\": \"1576893\", \"from_node_id\": \"1576525\", \"b2\": {\"y\": 1668.25, \"x\": 1055.0}, \"b1\": {\"y\": 1617.5, \"x\": 1055.0}}, \"405\": {\"to_node_id\": \"1576892\", \"from_node_id\": \"1576891\", \"b2\": null, \"b1\": null}, \"404\": {\"to_node_id\": \"1576891\", \"from_node_id\": \"1576893\", \"b2\": null, \"b1\": null}, \"410\": {\"to_node_id\": \"1576574\", \"from_node_id\": \"1576892\", \"b2\": {\"y\": 1835.2079728939614, \"x\": 1055.0}, \"b1\": {\"y\": 1793.0623918681883, \"x\": 1055.0}}, \"409\": {\"to_node_id\": \"1576529\", \"from_node_id\": \"1576892\", \"b2\": {\"y\": 1850.0, \"x\": 1055.0}, \"b1\": {\"y\": 1797.5, \"x\": 1055.0}}, \"408\": {\"to_node_id\": \"1576573\", \"from_node_id\": \"1576892\", \"b2\": {\"y\": 1822.4341649025257, \"x\": 1055.0}, \"b1\": {\"y\": 1789.2302494707578, \"x\": 1055.0}}}, \"genes\": [{\"bigg_id\": \"b1723\", \"name\": \"pfkB\"}, {\"bigg_id\": \"b3916\", \"name\": \"pfkA\"}], \"reversibility\": false, \"metabolites\": [{\"coefficient\": 1.0, \"bigg_id\": \"h_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"adp_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"f6p_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"atp_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"fdp_c\"}], \"label_x\": 1065.0, \"label_y\": 1725.0, \"gene_reaction_rule\": \"b3916 or b1723\"}, \"1576720\": {\"name\": \"Succinyl-CoA synthetase (ADP-forming)\", \"bigg_id\": \"SUCOAS\", \"segments\": {\"397\": {\"to_node_id\": \"1576890\", \"from_node_id\": \"1576888\", \"b2\": null, \"b1\": null}, \"396\": {\"to_node_id\": \"1576888\", \"from_node_id\": \"1576889\", \"b2\": null, \"b1\": null}, \"401\": {\"to_node_id\": \"1576569\", \"from_node_id\": \"1576890\", \"b2\": {\"y\": 3112.853617863234, \"x\": 3706.5785261677484}, \"b1\": {\"y\": 3077.95608535897, \"x\": 3678.1735578503244}}, \"400\": {\"to_node_id\": \"1576889\", \"from_node_id\": \"1576533\", \"b2\": {\"y\": 2954.5162418879845, \"x\": 3567.004509117803}, \"b1\": {\"y\": 2904.387472959949, \"x\": 3518.015030392677}}, \"403\": {\"to_node_id\": \"1576571\", \"from_node_id\": \"1576890\", \"b2\": {\"y\": 3107.574578569538, \"x\": 3702.2816337193913}, \"b1\": {\"y\": 3076.3723735708613, \"x\": 3676.8844901158172}}, \"399\": {\"to_node_id\": \"1576889\", \"from_node_id\": \"1576568\", \"b2\": {\"y\": 2964.923830429043, \"x\": 3577.1755615556553}, \"b1\": {\"y\": 2939.079434763476, \"x\": 3551.9185385188516}}, \"398\": {\"to_node_id\": \"1576889\", \"from_node_id\": \"1576567\", \"b2\": {\"y\": 2962.1615434463, \"x\": 3574.4760538225205}, \"b1\": {\"y\": 2929.871811487667, \"x\": 3542.920179408402}}, \"402\": {\"to_node_id\": \"1576570\", \"from_node_id\": \"1576890\", \"b2\": {\"y\": 3120.815793594307, \"x\": 3713.059366879087}, \"b1\": {\"y\": 3080.344738078292, \"x\": 3680.117810063726}}}, \"genes\": [{\"bigg_id\": \"b0729\", \"name\": \"sucD\"}, {\"bigg_id\": \"b0728\", \"name\": \"sucC\"}], \"reversibility\": true, \"metabolites\": [{\"coefficient\": -1.0, \"bigg_id\": \"coa_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"adp_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"succ_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"atp_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"succoa_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"pi_c\"}], \"label_x\": 3641.0, \"label_y\": 3010.0, \"gene_reaction_rule\": \"b0728 and b0729\"}, \"1576723\": {\"name\": \"Transketolase\", \"bigg_id\": \"TKT2\", \"segments\": {\"414\": {\"to_node_id\": \"1576898\", \"from_node_id\": \"1576896\", \"b2\": null, \"b1\": null}, \"415\": {\"to_node_id\": \"1576897\", \"from_node_id\": \"1576547\", \"b2\": {\"y\": 1735.0, \"x\": 1772.8736958157247}, \"b1\": {\"y\": 1735.0, \"x\": 1954.5789860524153}}, \"416\": {\"to_node_id\": \"1576897\", \"from_node_id\": \"1576558\", \"b2\": {\"y\": 1735.0, \"x\": 1741.208765402248}, \"b1\": {\"y\": 1735.0, \"x\": 1849.0292180074937}}, \"417\": {\"to_node_id\": \"1576525\", \"from_node_id\": \"1576898\", \"b2\": {\"y\": 1587.43115234375, \"x\": 1280.4546812716637}, \"b1\": {\"y\": 1741.3470458984375, \"x\": 1235.776382408843}}, \"413\": {\"to_node_id\": \"1576896\", \"from_node_id\": \"1576897\", \"b2\": null, \"b1\": null}, \"418\": {\"to_node_id\": \"1576575\", \"from_node_id\": \"1576898\", \"b2\": {\"y\": 1735.0, \"x\": 1312.7516245895065}, \"b1\": {\"y\": 1735.0, \"x\": 1318.2979239002893}}}, \"genes\": [{\"bigg_id\": \"b2465\", \"name\": \"tktB\"}, {\"bigg_id\": \"b2935\", \"name\": \"tktA\"}], \"reversibility\": true, \"metabolites\": [{\"coefficient\": -1.0, \"bigg_id\": \"xu5p__D_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"e4p_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"g3p_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"f6p_c\"}], \"label_x\": 1520.71923828125, \"label_y\": 1702.785400390625, \"gene_reaction_rule\": \"b2935 or b2465\"}, \"1576722\": {\"name\": \"L-Glutamate exchange\", \"bigg_id\": \"EX_glu__L_e\", \"segments\": {\"411\": {\"to_node_id\": \"1576895\", \"from_node_id\": \"1576552\", \"b2\": {\"y\": 3635.0, \"x\": 4949.0}, \"b1\": {\"y\": 3634.3, \"x\": 4902.8}}}, \"genes\": [], \"reversibility\": false, \"metabolites\": [{\"coefficient\": -1.0, \"bigg_id\": \"glu__L_e\"}], \"label_x\": 4936.36669921875, \"label_y\": 3680.79150390625, \"gene_reaction_rule\": \"\"}, \"1576729\": {\"name\": \"O2 exchange\", \"bigg_id\": \"EX_o2_e\", \"segments\": {\"454\": {\"to_node_id\": \"1576914\", \"from_node_id\": \"1576494\", \"b2\": {\"y\": 1660.0, \"x\": 4910.0}, \"b1\": {\"y\": 1660.0, \"x\": 4868.0}}}, \"genes\": [], \"reversibility\": true, \"metabolites\": [{\"coefficient\": -1.0, \"bigg_id\": \"o2_e\"}], \"label_x\": 4894.58984375, \"label_y\": 1633.8848876953125, \"gene_reaction_rule\": \"\"}, \"1576728\": {\"name\": \"Pyruvate formate lyase\", \"bigg_id\": \"PFL\", \"segments\": {\"449\": {\"to_node_id\": \"1576913\", \"from_node_id\": \"1576911\", \"b2\": null, \"b1\": null}, \"448\": {\"to_node_id\": \"1576911\", \"from_node_id\": \"1576912\", \"b2\": null, \"b1\": null}, \"450\": {\"to_node_id\": \"1576912\", \"from_node_id\": \"1576589\", \"b2\": {\"y\": 3995.0, \"x\": 1239.5565547885194}, \"b1\": {\"y\": 3995.0, \"x\": 1203.521849295065}}, \"451\": {\"to_node_id\": \"1576912\", \"from_node_id\": \"1576511\", \"b2\": {\"y\": 3995.0, \"x\": 1224.0767078078675}, \"b1\": {\"y\": 3995.0, \"x\": 1151.9223593595584}}, \"452\": {\"to_node_id\": \"1576514\", \"from_node_id\": \"1576913\", \"b2\": {\"y\": 3995.0, \"x\": 1567.0690632574556}, \"b1\": {\"y\": 3995.0, \"x\": 1460.6207189772367}}, \"453\": {\"to_node_id\": \"1576590\", \"from_node_id\": \"1576913\", \"b2\": {\"y\": 3995.0, \"x\": 1469.8292804986534}, \"b1\": {\"y\": 3995.0, \"x\": 1431.4487841495961}}}, \"genes\": [{\"bigg_id\": \"b3114\", \"name\": \"tdcE\"}, {\"bigg_id\": \"b3951\", \"name\": \"pflD\"}, {\"bigg_id\": \"b3952\", \"name\": \"pflC\"}, {\"bigg_id\": \"b0903\", \"name\": \"pflB\"}, {\"bigg_id\": \"b0902\", \"name\": \"pflA\"}, {\"bigg_id\": \"b2579\", \"name\": \"grcA\"}], \"reversibility\": false, \"metabolites\": [{\"coefficient\": -1.0, \"bigg_id\": \"pyr_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"coa_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"accoa_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"for_c\"}], \"label_x\": 1310.09130859375, \"label_y\": 4046.8837890625, \"gene_reaction_rule\": \"((b0902 and b0903) and b2579) or (b0902 and b0903) or (b0902 and b3114) or (b3951 and b3952)\"}, \"1577532\": {\"name\": \"Malate transport via proton symport 2 H \", \"bigg_id\": \"MALt2_2\", \"segments\": {\"33\": {\"to_node_id\": \"1577056\", \"from_node_id\": \"1576831\", \"b2\": {\"y\": 720.0576036932963, \"x\": 2610.0}, \"b1\": {\"y\": 682.8586789776543, \"x\": 2610.0}}, \"32\": {\"to_node_id\": \"1577058\", \"from_node_id\": \"1577057\", \"b2\": null, \"b1\": null}, \"31\": {\"to_node_id\": \"1577057\", \"from_node_id\": \"1577056\", \"b2\": null, \"b1\": null}, \"36\": {\"to_node_id\": \"1576504\", \"from_node_id\": \"1577058\", \"b2\": {\"y\": 2086.013675322326, \"x\": 2610.0}, \"b1\": {\"y\": 1311.8041025966977, \"x\": 2610.0}}, \"35\": {\"to_node_id\": \"1576810\", \"from_node_id\": \"1577058\", \"b2\": {\"y\": 1032.2015325445527, \"x\": 2610.0}, \"b1\": {\"y\": 995.6604597633658, \"x\": 2610.0}}, \"34\": {\"to_node_id\": \"1577056\", \"from_node_id\": \"1576816\", \"b2\": {\"y\": 723.1, \"x\": 2610.0}, \"b1\": {\"y\": 693.0, \"x\": 2610.0}}}, \"genes\": [{\"bigg_id\": \"b3528\", \"name\": \"dctA\"}], \"reversibility\": false, \"metabolites\": [{\"coefficient\": 1.0, \"bigg_id\": \"mal__L_c\"}, {\"coefficient\": 2.0, \"bigg_id\": \"h_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"mal__L_e\"}, {\"coefficient\": -2.0, \"bigg_id\": \"h_e\"}], \"label_x\": 2623.173583984375, \"label_y\": 844.3793334960938, \"gene_reaction_rule\": \"b3528\"}, \"1577537\": {\"name\": \"L-Malate exchange\", \"bigg_id\": \"EX_mal__L_e\", \"segments\": {\"37\": {\"to_node_id\": \"1577059\", \"from_node_id\": \"1576816\", \"b2\": {\"y\": 560.0, \"x\": 2610.0}, \"b1\": {\"y\": 623.0, \"x\": 2610.0}}}, \"genes\": [], \"reversibility\": false, \"metabolites\": [{\"coefficient\": -1.0, \"bigg_id\": \"mal__L_e\"}], \"label_x\": 2356.597412109375, \"label_y\": 513.949951171875, \"gene_reaction_rule\": \"\"}, \"1576732\": {\"name\": \"2 oxoglutarate reversible transport via symport\", \"bigg_id\": \"AKGt2r\", \"segments\": {\"469\": {\"to_node_id\": \"1576924\", \"from_node_id\": \"1576923\", \"b2\": null, \"b1\": null}, \"472\": {\"to_node_id\": \"1576923\", \"from_node_id\": \"1576599\", \"b2\": {\"y\": 3228.0, \"x\": 4807.306616216525}, \"b1\": {\"y\": 3228.0, \"x\": 4843.0220540550845}}, \"473\": {\"to_node_id\": \"1576509\", \"from_node_id\": \"1576922\", \"b2\": {\"y\": 3228.0, \"x\": 4074.584218822}, \"b1\": {\"y\": 3228.0, \"x\": 4373.7752656466}}, \"470\": {\"to_node_id\": \"1576922\", \"from_node_id\": \"1576924\", \"b2\": null, \"b1\": null}, \"471\": {\"to_node_id\": \"1576923\", \"from_node_id\": \"1576598\", \"b2\": {\"y\": 3228.0, \"x\": 4811.5}, \"b1\": {\"y\": 3228.0, \"x\": 4857.0}}, \"474\": {\"to_node_id\": \"1576600\", \"from_node_id\": \"1576922\", \"b2\": {\"y\": 3228.0, \"x\": 4449.798467455447}, \"b1\": {\"y\": 3228.0, \"x\": 4486.339540236634}}}, \"genes\": [{\"bigg_id\": \"b2587\", \"name\": \"kgtP\"}], \"reversibility\": true, \"metabolites\": [{\"coefficient\": -1.0, \"bigg_id\": \"h_e\"}, {\"coefficient\": 1.0, \"bigg_id\": \"akg_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"akg_e\"}, {\"coefficient\": 1.0, \"bigg_id\": \"h_c\"}], \"label_x\": 4594.6474609375, \"label_y\": 3197.05029296875, \"gene_reaction_rule\": \"b2587\"}, \"1576733\": {\"name\": \"Fructose-bisphosphate aldolase\", \"bigg_id\": \"FBA\", \"segments\": {\"55\": {\"to_node_id\": \"1576601\", \"from_node_id\": \"1576925\", \"b2\": {\"y\": 2081.141357421875, \"x\": 929.645751953125}, \"b1\": {\"y\": 2082.5, \"x\": 1055.0}}, \"54\": {\"to_node_id\": \"1576575\", \"from_node_id\": \"1576925\", \"b2\": {\"y\": 2120.0, \"x\": 1055.0}, \"b1\": {\"y\": 2067.5, \"x\": 1055.0}}, \"53\": {\"to_node_id\": \"1576925\", \"from_node_id\": \"1576926\", \"b2\": null, \"b1\": null}, \"475\": {\"to_node_id\": \"1576926\", \"from_node_id\": \"1576529\", \"b2\": {\"y\": 1974.28076171875, \"x\": 1055.0}, \"b1\": {\"y\": 1970.4088134765625, \"x\": 1055.0}}}, \"genes\": [{\"bigg_id\": \"b2097\", \"name\": \"fbaB\"}, {\"bigg_id\": \"b2925\", \"name\": \"fbaA\"}, {\"bigg_id\": \"b1773\", \"name\": \"ydjI\"}], \"reversibility\": true, \"metabolites\": [{\"coefficient\": 1.0, \"bigg_id\": \"dhap_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"g3p_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"fdp_c\"}], \"label_x\": 969.7942504882812, \"label_y\": 2031.01611328125, \"gene_reaction_rule\": \"b2097 or b1773 or b2925\"}, \"1576730\": {\"name\": \"Phosphoenolpyruvate carboxykinase\", \"bigg_id\": \"PPCK\", \"segments\": {\"458\": {\"to_node_id\": \"1576917\", \"from_node_id\": \"1576591\", \"b2\": {\"y\": 3557.6148705803193, \"x\": 1495.4573041787805}, \"b1\": {\"y\": 3566.049568601064, \"x\": 1519.8576805959353}}, \"459\": {\"to_node_id\": \"1576917\", \"from_node_id\": \"1576521\", \"b2\": {\"y\": 3678.777694102544, \"x\": 1757.8986662821364}, \"b1\": {\"y\": 3758.886258987647, \"x\": 2521.6033016696215}}, \"461\": {\"to_node_id\": \"1576593\", \"from_node_id\": \"1576918\", \"b2\": {\"y\": 3483.931701701515, \"x\": 1277.7951051045454}, \"b1\": {\"y\": 3497.2795105104547, \"x\": 1317.8385315313635}}, \"460\": {\"to_node_id\": \"1576592\", \"from_node_id\": \"1576918\", \"b2\": {\"y\": 3492.675272400688, \"x\": 1304.0258172020633}, \"b1\": {\"y\": 3499.9025817202064, \"x\": 1325.707745160619}}, \"456\": {\"to_node_id\": \"1576916\", \"from_node_id\": \"1576917\", \"b2\": null, \"b1\": null}, \"457\": {\"to_node_id\": \"1576918\", \"from_node_id\": \"1576916\", \"b2\": null, \"b1\": null}, \"462\": {\"to_node_id\": \"1576517\", \"from_node_id\": \"1576918\", \"b2\": {\"y\": 3454.321462634956, \"x\": 1188.9643879048676}, \"b1\": {\"y\": 3488.3964387904866, \"x\": 1291.1893163714603}}}, \"genes\": [{\"bigg_id\": \"b3403\", \"name\": \"pck\"}], \"reversibility\": false, \"metabolites\": [{\"coefficient\": 1.0, \"bigg_id\": \"pep_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"atp_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"oaa_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"co2_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"adp_c\"}], \"label_x\": 1377.3328857421875, \"label_y\": 3600.33447265625, \"gene_reaction_rule\": \"b3403\"}, \"1576731\": {\"name\": \"Phosphate reversible transport via symport\", \"bigg_id\": \"PIt2r\", \"segments\": {\"468\": {\"to_node_id\": \"1576597\", \"from_node_id\": \"1576919\", \"b2\": {\"y\": 4475.0, \"x\": 2880.0}, \"b1\": {\"y\": 4506.5, \"x\": 2880.0}}, \"465\": {\"to_node_id\": \"1576921\", \"from_node_id\": \"1576594\", \"b2\": {\"y\": 4742.1, \"x\": 2880.0}, \"b1\": {\"y\": 4782.0, \"x\": 2880.0}}, \"464\": {\"to_node_id\": \"1576919\", \"from_node_id\": \"1576920\", \"b2\": null, \"b1\": null}, \"467\": {\"to_node_id\": \"1576596\", \"from_node_id\": \"1576919\", \"b2\": {\"y\": 4464.789040942943, \"x\": 2880.0}, \"b1\": {\"y\": 4503.436712282883, \"x\": 2880.0}}, \"466\": {\"to_node_id\": \"1576921\", \"from_node_id\": \"1576595\", \"b2\": {\"y\": 4740.280870394058, \"x\": 2880.0}, \"b1\": {\"y\": 4775.936234646861, \"x\": 2880.0}}, \"463\": {\"to_node_id\": \"1576920\", \"from_node_id\": \"1576921\", \"b2\": null, \"b1\": null}}, \"genes\": [{\"bigg_id\": \"b3493\", \"name\": \"pitA\"}, {\"bigg_id\": \"b2987\", \"name\": \"pitB\"}], \"reversibility\": true, \"metabolites\": [{\"coefficient\": -1.0, \"bigg_id\": \"h_e\"}, {\"coefficient\": 1.0, \"bigg_id\": \"h_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"pi_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"pi_e\"}], \"label_x\": 2916.97509765625, \"label_y\": 4649.33837890625, \"gene_reaction_rule\": \"b2987 or b3493\"}, \"1576736\": {\"name\": \"Ribose-5-phosphate isomerase\", \"bigg_id\": \"RPI\", \"segments\": {\"66\": {\"to_node_id\": \"1576931\", \"from_node_id\": \"1576605\", \"b2\": {\"y\": 1490.0, \"x\": 2362.0}, \"b1\": {\"y\": 1535.5, \"x\": 2401.9}}, \"67\": {\"to_node_id\": \"1576493\", \"from_node_id\": \"1576931\", \"b2\": {\"y\": 1345.0, \"x\": 2230.0}, \"b1\": {\"y\": 1401.0, \"x\": 2282.5}}}, \"genes\": [{\"bigg_id\": \"b2914\", \"name\": \"rpiA\"}, {\"bigg_id\": \"b4090\", \"name\": \"rpiB\"}], \"reversibility\": true, \"metabolites\": [{\"coefficient\": 1.0, \"bigg_id\": \"ru5p__D_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"r5p_c\"}], \"label_x\": 2315.0, \"label_y\": 1415.0, \"gene_reaction_rule\": \"b2914 or b4090\"}, \"1576737\": {\"name\": \"Malate synthase\", \"bigg_id\": \"MALS\", \"segments\": {\"75\": {\"to_node_id\": \"1576504\", \"from_node_id\": \"1576932\", \"b2\": {\"y\": 3264.9901959981144, \"x\": 2733.0283686781227}, \"b1\": {\"y\": 3318.197058799434, \"x\": 2810.0085106034367}}, \"74\": {\"to_node_id\": \"1576609\", \"from_node_id\": \"1576932\", \"b2\": {\"y\": 3288.6581354094164, \"x\": 2767.2713448476666}, \"b1\": {\"y\": 3325.297440622825, \"x\": 2820.2814034543}}, \"73\": {\"to_node_id\": \"1576608\", \"from_node_id\": \"1576932\", \"b2\": {\"y\": 3305.691868266011, \"x\": 2791.915894512527}, \"b1\": {\"y\": 3330.4075604798036, \"x\": 2827.674768353758}}, \"72\": {\"to_node_id\": \"1576933\", \"from_node_id\": \"1576607\", \"b2\": {\"y\": 3460.23788356471, \"x\": 3016.1884971205427}, \"b1\": {\"y\": 3495.7929452157005, \"x\": 3067.961657068476}}, \"71\": {\"to_node_id\": \"1576933\", \"from_node_id\": \"1576606\", \"b2\": {\"y\": 3455.108224908018, \"x\": 3008.7189941643073}, \"b1\": {\"y\": 3478.694083026728, \"x\": 3043.063313881025}}, \"70\": {\"to_node_id\": \"1576933\", \"from_node_id\": \"1576532\", \"b2\": {\"y\": 3464.029731004742, \"x\": 3021.7099591823435}, \"b1\": {\"y\": 3508.4324366824735, \"x\": 3086.3665306078124}}, \"68\": {\"to_node_id\": \"1576934\", \"from_node_id\": \"1576933\", \"b2\": null, \"b1\": null}, \"69\": {\"to_node_id\": \"1576932\", \"from_node_id\": \"1576934\", \"b2\": null, \"b1\": null}}, \"genes\": [{\"bigg_id\": \"b2976\", \"name\": \"glcB\"}, {\"bigg_id\": \"b4014\", \"name\": \"aceB\"}], \"reversibility\": false, \"metabolites\": [{\"coefficient\": 1.0, \"bigg_id\": \"coa_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"h_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"mal__L_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"accoa_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"h2o_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"glx_c\"}], \"label_x\": 2921.0, \"label_y\": 3378.0, \"gene_reaction_rule\": \"b4014 or b2976\"}, \"1576734\": {\"name\": \"Triose-phosphate isomerase\", \"bigg_id\": \"TPI\", \"segments\": {\"57\": {\"to_node_id\": \"1576575\", \"from_node_id\": \"1576927\", \"b2\": {\"y\": 2195.0, \"x\": 1005.0}, \"b1\": {\"y\": 2195.0, \"x\": 970.0}}, \"56\": {\"to_node_id\": \"1576927\", \"from_node_id\": \"1576601\", \"b2\": {\"y\": 2195.0, \"x\": 911.3470458984375}, \"b1\": {\"y\": 2195.0, \"x\": 911.255859375}}}, \"genes\": [{\"bigg_id\": \"b3919\", \"name\": \"tpiA\"}], \"reversibility\": true, \"metabolites\": [{\"coefficient\": -1.0, \"bigg_id\": \"dhap_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"g3p_c\"}], \"label_x\": 936.438232421875, \"label_y\": 2245.297119140625, \"gene_reaction_rule\": \"b3919\"}, \"1576735\": {\"name\": \"Cytochrome oxidase bd (ubiquinol-8: 2 protons)\", \"bigg_id\": \"CYTBD\", \"segments\": {\"59\": {\"to_node_id\": \"1576929\", \"from_node_id\": \"1576928\", \"b2\": null, \"b1\": null}, \"58\": {\"to_node_id\": \"1576928\", \"from_node_id\": \"1576930\", \"b2\": null, \"b1\": null}, \"60\": {\"to_node_id\": \"1576930\", \"from_node_id\": \"1576602\", \"b2\": {\"y\": 1900.0, \"x\": 4595.5}, \"b1\": {\"y\": 1895.1654052734375, \"x\": 4416.69775390625}}, \"61\": {\"to_node_id\": \"1576930\", \"from_node_id\": \"1576495\", \"b2\": {\"y\": 1900.0, \"x\": 4572.371881863104}, \"b1\": {\"y\": 1900.0, \"x\": 4437.906272877015}}, \"62\": {\"to_node_id\": \"1576930\", \"from_node_id\": \"1576542\", \"b2\": {\"y\": 1900.0, \"x\": 4581.906341374356}, \"b1\": {\"y\": 1890.3310546875, \"x\": 4368.162902237436}}, \"63\": {\"to_node_id\": \"1576539\", \"from_node_id\": \"1576929\", \"b2\": {\"y\": 1903.22314453125, \"x\": 4842.38515985191}, \"b1\": {\"y\": 1900.0, \"x\": 4715.894014752448}}, \"64\": {\"to_node_id\": \"1576603\", \"from_node_id\": \"1576929\", \"b2\": {\"y\": 1900.0, \"x\": 4775.0}, \"b1\": {\"y\": 1900.0, \"x\": 4715.5}}, \"65\": {\"to_node_id\": \"1576604\", \"from_node_id\": \"1576929\", \"b2\": {\"y\": 1758.1868896484375, \"x\": 4884.545828600423}, \"b1\": {\"y\": 1901.611572265625, \"x\": 4821.203885298877}}}, \"genes\": [{\"bigg_id\": \"b0978\", \"name\": \"cbdA\"}, {\"bigg_id\": \"b0734\", \"name\": \"cydB\"}, {\"bigg_id\": \"b0979\", \"name\": \"cbdB\"}, {\"bigg_id\": \"b0733\", \"name\": \"cydA\"}], \"reversibility\": false, \"metabolites\": [{\"coefficient\": -0.5, \"bigg_id\": \"o2_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"q8_c\"}, {\"coefficient\": 2.0, \"bigg_id\": \"h_e\"}, {\"coefficient\": -1.0, \"bigg_id\": \"q8h2_c\"}, {\"coefficient\": -2.0, \"bigg_id\": \"h_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"h2o_c\"}], \"label_x\": 4560.4169921875, \"label_y\": 1954.46044921875, \"gene_reaction_rule\": \"(b0978 and b0979) or (b0733 and b0734)\"}, \"1576739\": {\"name\": \"Phosphotransacetylase\", \"bigg_id\": \"PTAr\", \"segments\": {\"77\": {\"to_node_id\": \"1576936\", \"from_node_id\": \"1576937\", \"b2\": null, \"b1\": null}, \"76\": {\"to_node_id\": \"1576937\", \"from_node_id\": \"1576935\", \"b2\": null, \"b1\": null}, \"80\": {\"to_node_id\": \"1576613\", \"from_node_id\": \"1576936\", \"b2\": {\"y\": 4140.0, \"x\": 1715.0}, \"b1\": {\"y\": 4115.5, \"x\": 1715.0}}, \"81\": {\"to_node_id\": \"1576614\", \"from_node_id\": \"1576936\", \"b2\": {\"y\": 4157.201532544553, \"x\": 1715.0}, \"b1\": {\"y\": 4120.660459763366, \"x\": 1715.0}}, \"79\": {\"to_node_id\": \"1576935\", \"from_node_id\": \"1576612\", \"b2\": {\"y\": 4049.339540236634, \"x\": 1715.0}, \"b1\": {\"y\": 4012.7984674554473, \"x\": 1715.0}}, \"78\": {\"to_node_id\": \"1576935\", \"from_node_id\": \"1576514\", \"b2\": {\"y\": 4047.0, \"x\": 1715.0}, \"b1\": {\"y\": 4005.0, \"x\": 1715.0}}}, \"genes\": [{\"bigg_id\": \"b2297\", \"name\": \"pta\"}, {\"bigg_id\": \"b2458\", \"name\": \"eutD\"}], \"reversibility\": true, \"metabolites\": [{\"coefficient\": -1.0, \"bigg_id\": \"accoa_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"actp_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"pi_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"coa_c\"}], \"label_x\": 1725.0, \"label_y\": 4075.0, \"gene_reaction_rule\": \"b2297 or b2458\"}, \"1577529\": {\"name\": \"D-Fructose exchange\", \"bigg_id\": \"EX_fru_e\", \"segments\": {\"29\": {\"to_node_id\": \"1577054\", \"from_node_id\": \"1576826\", \"b2\": {\"y\": 1360.0, \"x\": 241.0}, \"b1\": {\"y\": 1360.0, \"x\": 282.3}}}, \"genes\": [], \"reversibility\": false, \"metabolites\": [{\"coefficient\": -1.0, \"bigg_id\": \"fru_e\"}], \"label_x\": 125.35597229003906, \"label_y\": 1321.438232421875, \"gene_reaction_rule\": \"\"}, \"1577520\": {\"name\": \"Fumarate reductase\", \"bigg_id\": \"FRD7\", \"segments\": {\"20\": {\"to_node_id\": \"1577048\", \"from_node_id\": \"1576830\", \"b2\": {\"y\": 2600.0, \"x\": 3057.152680682408}, \"b1\": {\"y\": 2600.0, \"x\": 3003.842268941361}}, \"21\": {\"to_node_id\": \"1576814\", \"from_node_id\": \"1577050\", \"b2\": {\"y\": 2600.0, \"x\": 3213.824115301167}, \"b1\": {\"y\": 2600.0, \"x\": 3162.14723459035}}, \"22\": {\"to_node_id\": \"1576533\", \"from_node_id\": \"1577050\", \"b2\": {\"y\": 2600.0, \"x\": 3333.4483135103533}, \"b1\": {\"y\": 2600.0, \"x\": 3198.034494053106}}, \"19\": {\"to_node_id\": \"1577048\", \"from_node_id\": \"1576503\", \"b2\": {\"y\": 2600.0, \"x\": 3020.2689151278832}, \"b1\": {\"y\": 2600.0, \"x\": 2880.896383759611}}, \"18\": {\"to_node_id\": \"1577050\", \"from_node_id\": \"1577049\", \"b2\": null, \"b1\": null}, \"17\": {\"to_node_id\": \"1577049\", \"from_node_id\": \"1577048\", \"b2\": null, \"b1\": null}}, \"genes\": [{\"bigg_id\": \"b4152\", \"name\": \"frdC\"}, {\"bigg_id\": \"b4151\", \"name\": \"frdD\"}, {\"bigg_id\": \"b4154\", \"name\": \"frdA\"}, {\"bigg_id\": \"b4153\", \"name\": \"frdB\"}], \"reversibility\": false, \"metabolites\": [{\"coefficient\": 1.0, \"bigg_id\": \"succ_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"fum_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"q8_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"q8h2_c\"}], \"label_x\": 3078.744384765625, \"label_y\": 2572.54541015625, \"gene_reaction_rule\": \"b4151 and b4152 and b4153 and b4154\"}, \"1577524\": {\"name\": \"Fructose transport via PEPPyr PTS f6p generating \", \"bigg_id\": \"FRUpts2\", \"segments\": {\"24\": {\"to_node_id\": \"1577053\", \"from_node_id\": \"1577052\", \"b2\": null, \"b1\": null}, \"25\": {\"to_node_id\": \"1577051\", \"from_node_id\": \"1576826\", \"b2\": {\"y\": 1361.5867919921875, \"x\": 526.550048828125}, \"b1\": {\"y\": 1360.0, \"x\": 465.0}}, \"26\": {\"to_node_id\": \"1577051\", \"from_node_id\": \"1576819\", \"b2\": {\"y\": 1360.0, \"x\": 396.1378949970459}, \"b1\": {\"y\": 1452.0322265625, \"x\": 400.008457770101}}, \"27\": {\"to_node_id\": \"1576525\", \"from_node_id\": \"1577053\", \"b2\": {\"y\": 1361.586669921875, \"x\": 917.1118252292455}, \"b1\": {\"y\": 1363.1734619140625, \"x\": 860.370260215258}}, \"23\": {\"to_node_id\": \"1577052\", \"from_node_id\": \"1577051\", \"b2\": null, \"b1\": null}, \"28\": {\"to_node_id\": \"1576821\", \"from_node_id\": \"1577053\", \"b2\": {\"y\": 1240.992919921875, \"x\": 861.106575008484}, \"b1\": {\"y\": 1364.76025390625, \"x\": 861.4990623462952}}}, \"genes\": [{\"bigg_id\": \"b1819\", \"name\": \"manZ\"}, {\"bigg_id\": \"b1817\", \"name\": \"manX\"}, {\"bigg_id\": \"b2415\", \"name\": \"ptsH\"}, {\"bigg_id\": \"b2416\", \"name\": \"ptsI\"}, {\"bigg_id\": \"b1818\", \"name\": \"manY\"}], \"reversibility\": false, \"metabolites\": [{\"coefficient\": -1.0, \"bigg_id\": \"pep_c\"}, {\"coefficient\": 1.0, \"bigg_id\": \"f6p_c\"}, {\"coefficient\": -1.0, \"bigg_id\": \"fru_e\"}, {\"coefficient\": 1.0, \"bigg_id\": \"pyr_c\"}], \"label_x\": 456.41497802734375, \"label_y\": 1327.7852783203125, \"gene_reaction_rule\": \"b1817 and b1818 and b1819 and b2415 and b2416\"}}, \"nodes\": {\"1576509\": {\"node_is_primary\": true, \"name\": \"2-Oxoglutarate\", \"label_x\": 3669.712158203125, \"node_type\": \"metabolite\", \"y\": 3627.0, \"x\": 3746.0, \"bigg_id\": \"akg_c\", \"label_y\": 3611.942626953125}, \"1576508\": {\"node_is_primary\": false, \"name\": \"CO2\", \"label_x\": 3539.820068359375, \"node_type\": \"metabolite\", \"y\": 3710.0, \"x\": 3620.0, \"bigg_id\": \"co2_c\", \"label_y\": 3711.611572265625}, \"1576501\": {\"node_is_primary\": true, \"name\": \"Nicotinamide adenine dinucleotide\", \"label_x\": 3556.820068359375, \"node_type\": \"metabolite\", \"y\": 1147.3958740234375, \"x\": 3625.7841796875, \"bigg_id\": \"nad_c\", \"label_y\": 1187.06494140625}, \"1576500\": {\"node_is_primary\": true, \"name\": \"Nicotinamide adenine dinucleotide phosphate - reduced\", \"label_x\": 3740.0, \"node_type\": \"metabolite\", \"y\": 1270.0, \"x\": 3740.0, \"bigg_id\": \"nadph_c\", \"label_y\": 1310.0}, \"1576503\": {\"node_is_primary\": true, \"name\": \"Fumarate\", \"label_x\": 2867.189453125, \"node_type\": \"metabolite\", \"y\": 2920.0, \"x\": 2843.0, \"bigg_id\": \"fum_c\", \"label_y\": 2939.602783203125}, \"1576502\": {\"node_is_primary\": false, \"name\": \"H2O\", \"label_x\": 2693.4130859375, \"node_type\": \"metabolite\", \"y\": 2921.0, \"x\": 2715.0, \"bigg_id\": \"h2o_c\", \"label_y\": 2896.71923828125}, \"1576505\": {\"node_is_primary\": true, \"name\": \"Isocitrate\", \"label_x\": 3344.32373046875, \"node_type\": \"metabolite\", \"y\": 3976.0, \"x\": 3413.0, \"bigg_id\": \"icit_c\", \"label_y\": 3949.222900390625}, \"1576504\": {\"node_is_primary\": true, \"name\": \"L-Malate\", \"label_x\": 2506.83544921875, \"node_type\": \"metabolite\", \"y\": 3192.0, \"x\": 2621.0, \"bigg_id\": \"mal__L_c\", \"label_y\": 3174.826416015625}, \"1576507\": {\"node_is_primary\": false, \"name\": \"Nicotinamide adenine dinucleotide phosphate - reduced\", \"label_x\": 3557.6474609375, \"node_type\": \"metabolite\", \"y\": 3660.0, \"x\": 3659.0, \"bigg_id\": \"nadph_c\", \"label_y\": 3655.388427734375}, \"1576506\": {\"node_is_primary\": false, \"name\": \"Nicotinamide adenine dinucleotide phosphate\", \"label_x\": 3495.0, \"node_type\": \"metabolite\", \"y\": 4065.0, \"x\": 3495.0, \"bigg_id\": \"nadp_c\", \"label_y\": 4098.0}, \"1576992\": {\"y\": 980.0, \"x\": 2840.0, \"node_type\": \"multimarker\"}, \"1576868\": {\"y\": 4158.0, \"x\": 5020.0, \"node_type\": \"midmarker\"}, \"1576993\": {\"y\": 840.0, \"x\": 2840.0, \"node_type\": \"midmarker\"}, \"1576863\": {\"y\": 2880.0, \"x\": 5025.0, \"node_type\": \"midmarker\"}, \"1576860\": {\"y\": 1665.0, \"x\": 835.0, \"node_type\": \"multimarker\"}, \"1576861\": {\"y\": 1765.0, \"x\": 835.0, \"node_type\": \"multimarker\"}, \"1576866\": {\"y\": 1260.0, \"x\": 4145.0, \"node_type\": \"midmarker\"}, \"1576867\": {\"y\": 1200.0, \"x\": 4145.0, \"node_type\": \"multimarker\"}, \"1576864\": {\"y\": 3766.0, \"x\": 3417.0, \"node_type\": \"multimarker\"}, \"1576865\": {\"y\": 3857.0, \"x\": 3417.0, \"node_type\": \"midmarker\"}, \"1576490\": {\"node_is_primary\": true, \"name\": \"6-Phospho-D-gluconate\", \"label_x\": 1800.0250244140625, \"node_type\": \"metabolite\", \"y\": 1265.0, \"x\": 1827.0, \"bigg_id\": \"6pgc_c\", \"label_y\": 1235.0}, \"1576996\": {\"y\": 4501.49560546875, \"x\": 1715.0, \"node_type\": \"multimarker\"}, \"1576997\": {\"y\": 1008.8675537109375, \"x\": 1056.5865478515625, \"node_type\": \"midmarker\"}, \"1576994\": {\"y\": 4745.0, \"x\": 1715.0, \"node_type\": \"multimarker\"}, \"1576995\": {\"y\": 4585.0, \"x\": 1715.0, \"node_type\": \"midmarker\"}, \"1576615\": {\"node_is_primary\": false, \"name\": \"ATP\", \"label_x\": 4552.7265625, \"node_type\": \"metabolite\", \"y\": 4258.0, \"x\": 4535.0, \"bigg_id\": \"atp_c\", \"label_y\": 4269.94287109375}, \"1576614\": {\"node_is_primary\": false, \"name\": \"Coenzyme A\", \"label_x\": 1835.0, \"node_type\": \"metabolite\", \"y\": 4135.0, \"x\": 1815.0, \"bigg_id\": \"coa_c\", \"label_y\": 4135.0}, \"1576617\": {\"node_is_primary\": false, \"name\": \"Phosphate\", \"label_x\": 4268.884765625, \"node_type\": \"metabolite\", \"y\": 4258.0, \"x\": 4285.0, \"bigg_id\": \"pi_c\", \"label_y\": 4287.6689453125}, \"1576616\": {\"node_is_primary\": false, \"name\": \"H2O\", \"label_x\": 4481.77685546875, \"node_type\": \"metabolite\", \"y\": 4258.0, \"x\": 4485.0, \"bigg_id\": \"h2o_c\", \"label_y\": 4287.6689453125}, \"1576613\": {\"node_is_primary\": true, \"name\": \"Acetyl phosphate\", \"label_x\": 1745.0, \"node_type\": \"metabolite\", \"y\": 4175.0, \"x\": 1715.0, \"bigg_id\": \"actp_c\", \"label_y\": 4175.0}, \"1576612\": {\"node_is_primary\": false, \"name\": \"Phosphate\", \"label_x\": 1835.0, \"node_type\": \"metabolite\", \"y\": 4035.0, \"x\": 1815.0, \"bigg_id\": \"pi_c\", \"label_y\": 4035.0}, \"1576619\": {\"node_is_primary\": false, \"name\": \"H+\", \"label_x\": 4331.77734375, \"node_type\": \"metabolite\", \"y\": 4258.0, \"x\": 4335.0, \"bigg_id\": \"h_c\", \"label_y\": 4286.0576171875}, \"1576618\": {\"node_is_primary\": false, \"name\": \"ADP\", \"label_x\": 4381.77685546875, \"node_type\": \"metabolite\", \"y\": 4258.0, \"x\": 4385.0, \"bigg_id\": \"adp_c\", \"label_y\": 4290.89208984375}, \"1576598\": {\"node_is_primary\": true, \"name\": \"2-Oxoglutarate\", \"label_x\": 4922.0, \"node_type\": \"metabolite\", \"y\": 3228.0, \"x\": 4922.0, \"bigg_id\": \"akg_e\", \"label_y\": 3268.0}, \"1576599\": {\"node_is_primary\": false, \"name\": \"H+\", \"label_x\": 4861.39599609375, \"node_type\": \"metabolite\", \"y\": 3321.0, \"x\": 4834.0, \"bigg_id\": \"h_e\", \"label_y\": 3334.553955078125}, \"1576596\": {\"node_is_primary\": false, \"name\": \"H+\", \"label_x\": 2754.13232421875, \"node_type\": \"metabolite\", \"y\": 4452.0, \"x\": 2793.0, \"bigg_id\": \"h_c\", \"label_y\": 4426.61181640625}, \"1576597\": {\"node_is_primary\": true, \"name\": \"Phosphate\", \"label_x\": 2921.0, \"node_type\": \"metabolite\", \"y\": 4430.0, \"x\": 2880.0, \"bigg_id\": \"pi_c\", \"label_y\": 4428.0}, \"1576594\": {\"node_is_primary\": true, \"name\": \"Phosphate\", \"label_x\": 2910.0, \"node_type\": \"metabolite\", \"y\": 4839.0, \"x\": 2880.0, \"bigg_id\": \"pi_e\", \"label_y\": 4839.0}, \"1576595\": {\"node_is_primary\": false, \"name\": \"H+\", \"label_x\": 2733.83544921875, \"node_type\": \"metabolite\", \"y\": 4778.0, \"x\": 2793.0, \"bigg_id\": \"h_e\", \"label_y\": 4763.8515625}, \"1576592\": {\"node_is_primary\": false, \"name\": \"ADP\", \"label_x\": 1285.0, \"node_type\": \"metabolite\", \"y\": 3545.0, \"x\": 1285.0, \"bigg_id\": \"adp_c\", \"label_y\": 3575.0}, \"1576593\": {\"node_is_primary\": false, \"name\": \"CO2\", \"label_x\": 1215.0, \"node_type\": \"metabolite\", \"y\": 3515.0, \"x\": 1215.0, \"bigg_id\": \"co2_c\", \"label_y\": 3535.0}, \"1576590\": {\"node_is_primary\": true, \"name\": \"Formate\", \"label_x\": 1495.230712890625, \"node_type\": \"metabolite\", \"y\": 4095.0, \"x\": 1460.0, \"bigg_id\": \"for_c\", \"label_y\": 4083.958984375}, \"1576591\": {\"node_is_primary\": false, \"name\": \"ATP\", \"label_x\": 1525.0, \"node_type\": \"metabolite\", \"y\": 3625.0, \"x\": 1505.0, \"bigg_id\": \"atp_c\", \"label_y\": 3625.0}, \"1576819\": {\"node_is_primary\": false, \"name\": \"Phosphoenolpyruvate\", \"label_x\": 554.497314453125, \"node_type\": \"metabolite\", \"y\": 1448.4132080078125, \"x\": 565.6046142578125, \"bigg_id\": \"pep_c\", \"label_y\": 1479.5206298828125}, \"1576817\": {\"node_is_primary\": true, \"name\": \"Acetaldehyde\", \"label_x\": 2210.0, \"node_type\": \"metabolite\", \"y\": 3945.0, \"x\": 2210.0, \"bigg_id\": \"acald_c\", \"label_y\": 3905.0}, \"1576816\": {\"node_is_primary\": true, \"name\": \"L-Malate\", \"label_x\": 2640.0, \"node_type\": \"metabolite\", \"y\": 650.0, \"x\": 2610.0, \"bigg_id\": \"mal__L_e\", \"label_y\": 630.0}, \"1576815\": {\"node_is_primary\": true, \"name\": \"Acetaldehyde\", \"label_x\": 2240.0, \"node_type\": \"metabolite\", \"y\": 4837.65283203125, \"x\": 2210.0, \"bigg_id\": \"acald_e\", \"label_y\": 4837.65283203125}, \"1576814\": {\"node_is_primary\": false, \"name\": \"Ubiquinone-8\", \"label_x\": 3228.082275390625, \"node_type\": \"metabolite\", \"y\": 2730.0, \"x\": 3210.0, \"bigg_id\": \"q8_c\", \"label_y\": 2736.34716796875}, \"1576813\": {\"node_is_primary\": false, \"name\": \"H+\", \"label_x\": 2406.0, \"node_type\": \"metabolite\", \"y\": 3980.0, \"x\": 2392.0, \"bigg_id\": \"h_c\", \"label_y\": 3965.0}, \"1576812\": {\"node_is_primary\": false, \"name\": \"Nicotinamide adenine dinucleotide - reduced\", \"label_x\": 1868.892578125, \"node_type\": \"metabolite\", \"y\": 3860.0, \"x\": 1890.0, \"bigg_id\": \"nadh_c\", \"label_y\": 3840.810302734375}, \"1576811\": {\"node_is_primary\": false, \"name\": \"H+\", \"label_x\": 1245.23974609375, \"node_type\": \"metabolite\", \"y\": 4440.0, \"x\": 1270.0, \"bigg_id\": \"h_c\", \"label_y\": 4424.13232421875}, \"1576810\": {\"node_is_primary\": false, \"name\": \"H+\", \"label_x\": 2473.50439453125, \"node_type\": \"metabolite\", \"y\": 1010.0, \"x\": 2510.0, \"bigg_id\": \"h_c\", \"label_y\": 1043.1734619140625}, \"1576692\": {\"node_is_primary\": false, \"name\": \"Nicotinamide adenine dinucleotide phosphate - reduced\", \"label_x\": 2220.0, \"node_type\": \"metabolite\", \"y\": 3540.0, \"x\": 2200.0, \"bigg_id\": \"nadph_c\", \"label_y\": 3550.0}, \"1577106\": {\"y\": 4020.0757924471372, \"x\": 3331.3033751186185, \"node_type\": \"multimarker\"}, \"1577104\": {\"y\": 4035.7165277261324, \"x\": 3294.488060371458, \"node_type\": \"multimarker\"}, \"1577105\": {\"y\": 4027.896160086635, \"x\": 3312.895717745038, \"node_type\": \"midmarker\"}, \"1577069\": {\"y\": 4506.7353515625, \"x\": 1053.4132080078125, \"node_type\": \"multimarker\"}, \"1577068\": {\"y\": 4583.8583984375, \"x\": 1055.0, \"node_type\": \"midmarker\"}, \"1577100\": {\"y\": 4030.8645761345497, \"x\": 3074.275683525833, \"node_type\": \"midmarker\"}, \"1577101\": {\"y\": 4038.0833974815746, \"x\": 3092.9274560517806, \"node_type\": \"multimarker\"}, \"1577065\": {\"y\": 2819.0, \"x\": 3129.0, \"node_type\": \"midmarker\"}, \"1577064\": {\"y\": 2817.0, \"x\": 3180.0, \"node_type\": \"multimarker\"}, \"1577067\": {\"y\": 4660.98193359375, \"x\": 1055.0, \"node_type\": \"multimarker\"}, \"1577066\": {\"y\": 2826.0, \"x\": 3078.0, \"node_type\": \"multimarker\"}, \"1577061\": {\"y\": 2570.0, \"x\": 4630.0, \"node_type\": \"multimarker\"}, \"1577063\": {\"y\": 2570.0, \"x\": 4710.0, \"node_type\": \"multimarker\"}, \"1577062\": {\"y\": 2570.0, \"x\": 4670.0, \"node_type\": \"midmarker\"}, \"1576884\": {\"y\": 3945.0, \"x\": 155.0, \"node_type\": \"midmarker\"}, \"1576885\": {\"y\": 1415.0, \"x\": 2045.0, \"node_type\": \"midmarker\"}, \"1576929\": {\"y\": 1900.0, \"x\": 4690.0, \"node_type\": \"multimarker\"}, \"1576887\": {\"y\": 2230.0, \"x\": 4986.0, \"node_type\": \"midmarker\"}, \"1576880\": {\"y\": 3635.0, \"x\": 4779.0, \"node_type\": \"multimarker\"}, \"1576881\": {\"y\": 4925.0, \"x\": 1055.0, \"node_type\": \"midmarker\"}, \"1576923\": {\"y\": 3228.0, \"x\": 4792.0, \"node_type\": \"multimarker\"}, \"1576922\": {\"y\": 3228.0, \"x\": 4502.0, \"node_type\": \"multimarker\"}, \"1576921\": {\"y\": 4725.0, \"x\": 2880.0, \"node_type\": \"multimarker\"}, \"1576920\": {\"y\": 4580.0, \"x\": 2880.0, \"node_type\": \"midmarker\"}, \"1576927\": {\"y\": 2195.0, \"x\": 955.0, \"node_type\": \"midmarker\"}, \"1576926\": {\"y\": 1995.0, \"x\": 1055.0, \"node_type\": \"midmarker\"}, \"1576925\": {\"y\": 2045.0, \"x\": 1055.0, \"node_type\": \"multimarker\"}, \"1576924\": {\"y\": 3228.0, \"x\": 4662.0, \"node_type\": \"midmarker\"}, \"1576491\": {\"node_is_primary\": false, \"name\": \"CO2\", \"label_x\": 2007.0, \"node_type\": \"metabolite\", \"y\": 1165.0, \"x\": 2007.0, \"bigg_id\": \"co2_c\", \"label_y\": 1145.0}, \"1577087\": {\"node_is_primary\": false, \"name\": \"Coenzyme A\", \"label_x\": 5175.48828125, \"node_type\": \"metabolite\", \"y\": 4931.947265625, \"x\": 5208.85205078125, \"bigg_id\": \"coa_c\", \"label_y\": 4975.3330078125}, \"1577086\": {\"node_is_primary\": false, \"name\": \"Nicotinamide adenine dinucleotide\", \"label_x\": 4860.95166015625, \"node_type\": \"metabolite\", \"y\": 4529.25048828125, \"x\": 4893.27099609375, \"bigg_id\": \"nad_c\", \"label_y\": 4501.22509765625}, \"1577085\": {\"node_is_primary\": false, \"name\": \"Pyruvate\", \"label_x\": 4962.07666015625, \"node_type\": \"metabolite\", \"y\": 4509.18115234375, \"x\": 4988.05859375, \"bigg_id\": \"pyr_c\", \"label_y\": 4488.5498046875}, \"1577084\": {\"node_is_primary\": false, \"name\": \"D-Fructose 6-phosphate\", \"label_x\": 5052.6396484375, \"node_type\": \"metabolite\", \"y\": 4498.61865234375, \"x\": 5087.0712890625, \"bigg_id\": \"f6p_c\", \"label_y\": 4477.9873046875}, \"1577083\": {\"node_is_primary\": false, \"name\": \"ATP\", \"label_x\": 5152.708984375, \"node_type\": \"metabolite\", \"y\": 4488.05615234375, \"x\": 5183.9716796875, \"bigg_id\": \"atp_c\", \"label_y\": 4461.08740234375}, \"1577082\": {\"node_is_primary\": false, \"name\": \"Alpha-D-Ribose 5-phosphate\", \"label_x\": 5390.7685546875, \"node_type\": \"metabolite\", \"y\": 4418.7158203125, \"x\": 5403.0185546875, \"bigg_id\": \"r5p_c\", \"label_y\": 4391.7470703125}, \"1577081\": {\"node_is_primary\": false, \"name\": \"H+\", \"label_x\": 5204.56640625, \"node_type\": \"metabolite\", \"y\": 4994.3544921875, \"x\": 5262.95361328125, \"bigg_id\": \"h_c\", \"label_y\": 5025.4677734375}, \"1577080\": {\"node_is_primary\": false, \"name\": \"D-Erythrose 4-phosphate\", \"label_x\": 5442.19677734375, \"node_type\": \"metabolite\", \"y\": 4466.33544921875, \"x\": 5437.546875, \"bigg_id\": \"e4p_c\", \"label_y\": 4450.9853515625}, \"1576851\": {\"y\": 3945.0, \"x\": 1555.0, \"node_type\": \"multimarker\"}, \"1577089\": {\"node_is_primary\": false, \"name\": \"D-Glucose 6-phosphate\", \"label_x\": 4679.8271484375, \"node_type\": \"metabolite\", \"y\": 4559.88134765625, \"x\": 4700.52734375, \"bigg_id\": \"g6p_c\", \"label_y\": 4537.1376953125}, \"1577088\": {\"node_is_primary\": false, \"name\": \"Oxaloacetate\", \"label_x\": 4770.38916015625, \"node_type\": \"metabolite\", \"y\": 4544.03759765625, \"x\": 4783.69580078125, \"bigg_id\": \"oaa_c\", \"label_y\": 4522.349609375}, \"1576850\": {\"y\": 3945.0, \"x\": 1495.0, \"node_type\": \"midmarker\"}, \"1576853\": {\"y\": 3519.0, \"x\": 1493.0, \"node_type\": \"multimarker\"}, \"1576852\": {\"y\": 3486.0, \"x\": 1414.0, \"node_type\": \"midmarker\"}, \"1576998\": {\"y\": 976.1735229492188, \"x\": 1057.0, \"node_type\": \"multimarker\"}, \"1576498\": {\"node_is_primary\": false, \"name\": \"H+\", \"label_x\": 3529.338134765625, \"node_type\": \"metabolite\", \"y\": 650.0, \"x\": 3510.0, \"bigg_id\": \"h_e\", \"label_y\": 639.6690673828125}, \"1576857\": {\"y\": 1395.0, \"x\": 1055.0, \"node_type\": \"midmarker\"}, \"1576856\": {\"y\": 3225.0, \"x\": 1055.0, \"node_type\": \"midmarker\"}, \"1576855\": {\"y\": 3265.0, \"x\": 1055.0, \"node_type\": \"multimarker\"}, \"1576854\": {\"y\": 3458.0, \"x\": 1348.0, \"node_type\": \"multimarker\"}, \"1576493\": {\"node_is_primary\": true, \"name\": \"D-Ribulose 5-phosphate\", \"label_x\": 2155.0, \"node_type\": \"metabolite\", \"y\": 1265.0, \"x\": 2155.0, \"bigg_id\": \"ru5p__D_c\", \"label_y\": 1235.0}, \"1576492\": {\"node_is_primary\": false, \"name\": \"Nicotinamide adenine dinucleotide phosphate - reduced\", \"label_x\": 2080.0, \"node_type\": \"metabolite\", \"y\": 1165.0, \"x\": 2057.0, \"bigg_id\": \"nadph_c\", \"label_y\": 1140.0}, \"1576859\": {\"y\": 1715.0, \"x\": 835.0, \"node_type\": \"midmarker\"}, \"1576858\": {\"y\": 4598.0, \"x\": 3989.0, \"node_type\": \"midmarker\"}, \"1576497\": {\"node_is_primary\": true, \"name\": \"Nicotinamide adenine dinucleotide - reduced\", \"label_x\": 3450.0, \"node_type\": \"metabolite\", \"y\": 1120.0, \"x\": 3420.0, \"bigg_id\": \"nadh_c\", \"label_y\": 1150.0}, \"1576496\": {\"node_is_primary\": true, \"name\": \"Nicotinamide adenine dinucleotide phosphate\", \"label_x\": 3330.0, \"node_type\": \"metabolite\", \"y\": 1270.0, \"x\": 3330.0, \"bigg_id\": \"nadp_c\", \"label_y\": 1310.0}, \"1576495\": {\"node_is_primary\": true, \"name\": \"O2\", \"label_x\": 4259.09326171875, \"node_type\": \"metabolite\", \"y\": 1660.0, \"x\": 4330.0, \"bigg_id\": \"o2_c\", \"label_y\": 1677.4388427734375}, \"1576494\": {\"node_is_primary\": true, \"name\": \"O2\", \"label_x\": 4840.3310546875, \"node_type\": \"metabolite\", \"y\": 1660.0, \"x\": 4850.0, \"bigg_id\": \"o2_e\", \"label_y\": 1702.89208984375}, \"1576549\": {\"node_is_primary\": false, \"name\": \"H+\", \"label_x\": 911.677978515625, \"node_type\": \"metabolite\", \"y\": 3625.0, \"x\": 965.0, \"bigg_id\": \"h_c\", \"label_y\": 3612.305908203125}, \"1576548\": {\"node_is_primary\": true, \"name\": \"D-Fructose 6-phosphate\", \"label_x\": 2367.636962890625, \"node_type\": \"metabolite\", \"y\": 2186.826416015625, \"x\": 2362.636962890625, \"bigg_id\": \"f6p_c\", \"label_y\": 2216.826416015625}, \"1576545\": {\"node_is_primary\": true, \"name\": \"Glyceraldehyde 3-phosphate\", \"label_x\": 2375.091552734375, \"node_type\": \"metabolite\", \"y\": 1865.0, \"x\": 2358.984130859375, \"bigg_id\": \"g3p_c\", \"label_y\": 1898.4132080078125}, \"1576544\": {\"node_is_primary\": true, \"name\": \"CO2\", \"label_x\": 3651.0, \"node_type\": \"metabolite\", \"y\": 4754.0, \"x\": 3621.0, \"bigg_id\": \"co2_e\", \"label_y\": 4754.0}, \"1576547\": {\"node_is_primary\": true, \"name\": \"D-Erythrose 4-phosphate\", \"label_x\": 1922.7852783203125, \"node_type\": \"metabolite\", \"y\": 2190.0, \"x\": 1945.0, \"bigg_id\": \"e4p_c\", \"label_y\": 2242.214599609375}, \"1576546\": {\"node_is_primary\": true, \"name\": \"Sedoheptulose 7-phosphate\", \"label_x\": 1955.8675537109375, \"node_type\": \"metabolite\", \"y\": 1865.0, \"x\": 1983.082275390625, \"bigg_id\": \"s7p_c\", \"label_y\": 1814.314697265625}, \"1576542\": {\"node_is_primary\": true, \"name\": \"Ubiquinol-8\", \"label_x\": 4411.00048828125, \"node_type\": \"metabolite\", \"y\": 2210.3310546875, \"x\": 4508.47509765625, \"bigg_id\": \"q8h2_c\", \"label_y\": 2217.395751953125}, \"1577102\": {\"node_is_primary\": false, \"name\": \"H2O\", \"label_x\": 3189.1795476587818, \"node_type\": \"metabolite\", \"y\": 3976.144484067998, \"x\": 3188.1170476587818, \"bigg_id\": \"h2o_c\", \"label_y\": 3930.712355161748}, \"1576648\": {\"node_is_primary\": false, \"name\": \"H+\", \"label_x\": 2620.0, \"node_type\": \"metabolite\", \"y\": 4760.0, \"x\": 2600.0, \"bigg_id\": \"h_e\", \"label_y\": 4760.0}, \"1576649\": {\"node_is_primary\": true, \"name\": \"Ethanol\", \"label_x\": 2530.0, \"node_type\": \"metabolite\", \"y\": 4830.0, \"x\": 2500.0, \"bigg_id\": \"etoh_e\", \"label_y\": 4830.0}, \"1576646\": {\"node_is_primary\": false, \"name\": \"Phosphate\", \"label_x\": 684.8515014648438, \"node_type\": \"metabolite\", \"y\": 3425.0, \"x\": 735.0, \"bigg_id\": \"pi_c\", \"label_y\": 3409.13232421875}, \"1576647\": {\"node_is_primary\": true, \"name\": \"D-Glucose\", \"label_x\": 1085.066162109375, \"node_type\": \"metabolite\", \"y\": 705.0, \"x\": 1055.066162109375, \"bigg_id\": \"glc__D_e\", \"label_y\": 705.0}, \"1576644\": {\"node_is_primary\": false, \"name\": \"H+\", \"label_x\": 672.1574096679688, \"node_type\": \"metabolite\", \"y\": 3475.0, \"x\": 735.0, \"bigg_id\": \"h_c\", \"label_y\": 3478.173583984375}, \"1576645\": {\"node_is_primary\": false, \"name\": \"AMP\", \"label_x\": 640.4221801757812, \"node_type\": \"metabolite\", \"y\": 3525.0, \"x\": 735.0, \"bigg_id\": \"amp_c\", \"label_y\": 3545.6279296875}, \"1576642\": {\"node_is_primary\": false, \"name\": \"ATP\", \"label_x\": 670.5706176757812, \"node_type\": \"metabolite\", \"y\": 3855.0, \"x\": 735.0, \"bigg_id\": \"atp_c\", \"label_y\": 3840.71923828125}, \"1576643\": {\"node_is_primary\": false, \"name\": \"H2O\", \"label_x\": 681.677978515625, \"node_type\": \"metabolite\", \"y\": 3805.0, \"x\": 735.0, \"bigg_id\": \"h2o_c\", \"label_y\": 3787.545654296875}, \"1576640\": {\"node_is_primary\": true, \"name\": \"H2O\", \"label_x\": 3287.0, \"node_type\": \"metabolite\", \"y\": 4434.0, \"x\": 3257.0, \"bigg_id\": \"h2o_c\", \"label_y\": 4434.0}, \"1576641\": {\"node_is_primary\": true, \"name\": \"CO2\", \"label_x\": 3651.0, \"node_type\": \"metabolite\", \"y\": 4434.0, \"x\": 3621.0, \"bigg_id\": \"co2_c\", \"label_y\": 4434.0}, \"1577103\": {\"node_is_primary\": true, \"name\": \"Cis-Aconitate\", \"label_x\": 3156.1556150985407, \"node_type\": \"metabolite\", \"y\": 4053.2845721909744, \"x\": 3197.4642088485407, \"bigg_id\": \"acon_C_c\", \"label_y\": 4098.026271409724}, \"1576991\": {\"y\": 730.0, \"x\": 2840.0, \"node_type\": \"multimarker\"}, \"1577034\": {\"y\": 4570.0, \"x\": 2210.0, \"node_type\": \"midmarker\"}, \"1577035\": {\"y\": 4944.0, \"x\": 2211.0, \"node_type\": \"midmarker\"}, \"1577032\": {\"y\": 3945.0, \"x\": 2010.0, \"node_type\": \"midmarker\"}, \"1577033\": {\"y\": 3945.0, \"x\": 1980.0, \"node_type\": \"multimarker\"}, \"1577030\": {\"y\": 4925.0, \"x\": 1460.0, \"node_type\": \"midmarker\"}, \"1577031\": {\"y\": 3945.0, \"x\": 2040.0, \"node_type\": \"multimarker\"}, \"1576974\": {\"y\": 543.4132690429688, \"x\": 1056.6529541015625, \"node_type\": \"midmarker\"}, \"1576975\": {\"y\": 4430.0, \"x\": 2500.0, \"node_type\": \"multimarker\"}, \"1576976\": {\"y\": 4550.0, \"x\": 2500.0, \"node_type\": \"midmarker\"}, \"1576977\": {\"y\": 4730.0, \"x\": 2500.0, \"node_type\": \"multimarker\"}, \"1576970\": {\"y\": 3805.0, \"x\": 835.0, \"node_type\": \"multimarker\"}, \"1576971\": {\"y\": 3525.0, \"x\": 835.0, \"node_type\": \"multimarker\"}, \"1576972\": {\"y\": 3645.0, \"x\": 835.0, \"node_type\": \"midmarker\"}, \"1576978\": {\"y\": 2995.0, \"x\": 1055.0, \"node_type\": \"midmarker\"}, \"1576989\": {\"y\": 4925.107421875, \"x\": 2880.0, \"node_type\": \"midmarker\"}, \"1576602\": {\"node_is_primary\": false, \"name\": \"H+\", \"label_x\": 4234.01416015625, \"node_type\": \"metabolite\", \"y\": 1895.1654052734375, \"x\": 4301.69775390625, \"bigg_id\": \"h_c\", \"label_y\": 1903.8848876953125}, \"1576603\": {\"node_is_primary\": false, \"name\": \"H+\", \"label_x\": 4858.38818359375, \"node_type\": \"metabolite\", \"y\": 1900.0, \"x\": 4860.0, \"bigg_id\": \"h_e\", \"label_y\": 1950.6187744140625}, \"1576600\": {\"node_is_primary\": false, \"name\": \"H+\", \"label_x\": 4490.337890625, \"node_type\": \"metabolite\", \"y\": 3328.0, \"x\": 4472.0, \"bigg_id\": \"h_c\", \"label_y\": 3342.330810546875}, \"1576601\": {\"node_is_primary\": true, \"name\": \"Dihydroxyacetone phosphate\", \"label_x\": 739.3148193359375, \"node_type\": \"metabolite\", \"y\": 2195.0, \"x\": 855.0, \"bigg_id\": \"dhap_c\", \"label_y\": 2179.132568359375}, \"1576606\": {\"node_is_primary\": false, \"name\": \"Acetyl-CoA\", \"label_x\": 3125.0, \"node_type\": \"metabolite\", \"y\": 3402.0, \"x\": 3105.0, \"bigg_id\": \"accoa_c\", \"label_y\": 3402.0}, \"1576607\": {\"node_is_primary\": false, \"name\": \"H2O\", \"label_x\": 3190.0, \"node_type\": \"metabolite\", \"y\": 3480.0, \"x\": 3170.0, \"bigg_id\": \"h2o_c\", \"label_y\": 3480.0}, \"1576604\": {\"node_is_primary\": false, \"name\": \"H2O\", \"label_x\": 4452.77001953125, \"node_type\": \"metabolite\", \"y\": 1760.9495849609375, \"x\": 4439.09375, \"bigg_id\": \"h2o_c\", \"label_y\": 1736.77685546875}, \"1576605\": {\"node_is_primary\": true, \"name\": \"Alpha-D-Ribose 5-phosphate\", \"label_x\": 2418.413330078125, \"node_type\": \"metabolite\", \"y\": 1555.0, \"x\": 2419.0, \"bigg_id\": \"r5p_c\", \"label_y\": 1601.1072998046875}, \"1576608\": {\"node_is_primary\": false, \"name\": \"H+\", \"label_x\": 2891.0, \"node_type\": \"metabolite\", \"y\": 3220.0, \"x\": 2871.0, \"bigg_id\": \"h_c\", \"label_y\": 3220.0}, \"1576609\": {\"node_is_primary\": false, \"name\": \"Coenzyme A\", \"label_x\": 2800.0, \"node_type\": \"metabolite\", \"y\": 3168.0, \"x\": 2780.0, \"bigg_id\": \"coa_c\", \"label_y\": 3168.0}, \"1576808\": {\"node_is_primary\": false, \"name\": \"H+\", \"label_x\": 4874.1728515625, \"node_type\": \"metabolite\", \"y\": 2570.0, \"x\": 4850.0, \"bigg_id\": \"h_e\", \"label_y\": 2601.280517578125}, \"1576809\": {\"node_is_primary\": true, \"name\": \"Nicotinamide adenine dinucleotide\", \"label_x\": 4356.72412109375, \"node_type\": \"metabolite\", \"y\": 2646.77685546875, \"x\": 4449.00732421875, \"bigg_id\": \"nad_c\", \"label_y\": 2646.95849609375}, \"1576516\": {\"node_is_primary\": false, \"name\": \"H2O\", \"label_x\": 1315.0, \"node_type\": \"metabolite\", \"y\": 3375.0, \"x\": 1295.0, \"bigg_id\": \"h2o_c\", \"label_y\": 3375.0}, \"1576517\": {\"node_is_primary\": true, \"name\": \"Phosphoenolpyruvate\", \"label_x\": 1085.0, \"node_type\": \"metabolite\", \"y\": 3375.0, \"x\": 1055.0, \"bigg_id\": \"pep_c\", \"label_y\": 3355.0}, \"1576514\": {\"node_is_primary\": true, \"name\": \"Acetyl-CoA\", \"label_x\": 1695.0, \"node_type\": \"metabolite\", \"y\": 3945.0, \"x\": 1715.0, \"bigg_id\": \"accoa_c\", \"label_y\": 3965.0}, \"1576515\": {\"node_is_primary\": false, \"name\": \"Nicotinamide adenine dinucleotide - reduced\", \"label_x\": 1647.545654296875, \"node_type\": \"metabolite\", \"y\": 3885.0, \"x\": 1655.0, \"bigg_id\": \"nadh_c\", \"label_y\": 3855.4794921875}, \"1576512\": {\"node_is_primary\": false, \"name\": \"Coenzyme A\", \"label_x\": 1287.066162109375, \"node_type\": \"metabolite\", \"y\": 3905.0, \"x\": 1315.0, \"bigg_id\": \"coa_c\", \"label_y\": 3887.545654296875}, \"1576513\": {\"node_is_primary\": false, \"name\": \"CO2\", \"label_x\": 1595.0, \"node_type\": \"metabolite\", \"y\": 3855.0, \"x\": 1605.0, \"bigg_id\": \"co2_c\", \"label_y\": 3835.0}, \"1576510\": {\"node_is_primary\": false, \"name\": \"Nicotinamide adenine dinucleotide\", \"label_x\": 1394.189697265625, \"node_type\": \"metabolite\", \"y\": 3855.0, \"x\": 1365.0, \"bigg_id\": \"nad_c\", \"label_y\": 3851.826416015625}, \"1576511\": {\"node_is_primary\": true, \"name\": \"Pyruvate\", \"label_x\": 1025.0, \"node_type\": \"metabolite\", \"y\": 3945.0, \"x\": 1055.0, \"bigg_id\": \"pyr_c\", \"label_y\": 3965.0}, \"1576982\": {\"y\": 4275.0, \"x\": 1715.0, \"node_type\": \"midmarker\"}, \"1576691\": {\"node_is_primary\": false, \"name\": \"CO2\", \"label_x\": 2280.0, \"node_type\": \"metabolite\", \"y\": 3510.0, \"x\": 2260.0, \"bigg_id\": \"co2_c\", \"label_y\": 3510.0}, \"1576690\": {\"node_is_primary\": false, \"name\": \"Nicotinamide adenine dinucleotide phosphate\", \"label_x\": 2450.0, \"node_type\": \"metabolite\", \"y\": 3370.0, \"x\": 2430.0, \"bigg_id\": \"nadp_c\", \"label_y\": 3370.0}, \"1576518\": {\"node_is_primary\": false, \"name\": \"CO2\", \"label_x\": 1215.0, \"node_type\": \"metabolite\", \"y\": 3345.0, \"x\": 1205.0, \"bigg_id\": \"co2_c\", \"label_y\": 3325.0}, \"1576519\": {\"node_is_primary\": false, \"name\": \"H+\", \"label_x\": 1635.0, \"node_type\": \"metabolite\", \"y\": 3475.0, \"x\": 1615.0, \"bigg_id\": \"h_c\", \"label_y\": 3475.0}, \"1577078\": {\"node_is_primary\": false, \"name\": \"L-Glutamate\", \"label_x\": 5555.015625, \"node_type\": \"metabolite\", \"y\": 4651.92236328125, \"x\": 5534.521484375, \"bigg_id\": \"glu__L_c\", \"label_y\": 4645.02197265625}, \"1577079\": {\"node_is_primary\": false, \"name\": \"Glyceraldehyde 3-phosphate\", \"label_x\": 5534.87109375, \"node_type\": \"metabolite\", \"y\": 4517.58447265625, \"x\": 5512.26513671875, \"bigg_id\": \"g3p_c\", \"label_y\": 4508.57177734375}, \"1577072\": {\"node_is_primary\": false, \"name\": \"H+\", \"label_x\": 1123.4132080078125, \"node_type\": \"metabolite\", \"y\": 4416.7353515625, \"x\": 1093.4132080078125, \"bigg_id\": \"h_c\", \"label_y\": 4426.7353515625}, \"1577073\": {\"node_is_primary\": false, \"name\": \"H2O\", \"label_x\": 5539.50146484375, \"node_type\": \"metabolite\", \"y\": 4574.02490234375, \"x\": 5527.4580078125, \"bigg_id\": \"h2o_c\", \"label_y\": 4562.89990234375}, \"1577071\": {\"node_is_primary\": false, \"name\": \"H+\", \"label_x\": 1125.0, \"node_type\": \"metabolite\", \"y\": 4750.98193359375, \"x\": 1095.0, \"bigg_id\": \"h_e\", \"label_y\": 4760.98193359375}, \"1577076\": {\"y\": 4757.48193359375, \"x\": 5334.0517578125, \"node_type\": \"multimarker\"}, \"1577077\": {\"node_is_primary\": false, \"name\": \"ADP\", \"label_x\": 5529.1103515625, \"node_type\": \"metabolite\", \"y\": 4924.5068359375, \"x\": 5503.33544921875, \"bigg_id\": \"adp_c\", \"label_y\": 4931.337890625}, \"1577074\": {\"y\": 4717.48193359375, \"x\": 5334.0517578125, \"node_type\": \"multimarker\"}, \"1577075\": {\"y\": 4737.48193359375, \"x\": 5334.0517578125, \"node_type\": \"midmarker\"}, \"1576677\": {\"node_is_primary\": false, \"name\": \"Nicotinamide adenine dinucleotide phosphate - reduced\", \"label_x\": 3935.431640625, \"node_type\": \"metabolite\", \"y\": 3558.0, \"x\": 3992.0, \"bigg_id\": \"nadph_c\", \"label_y\": 3532.22314453125}, \"1576676\": {\"node_is_primary\": false, \"name\": \"Nicotinamide adenine dinucleotide phosphate\", \"label_x\": 4142.0, \"node_type\": \"metabolite\", \"y\": 3558.0, \"x\": 4142.0, \"bigg_id\": \"nadp_c\", \"label_y\": 3528.0}, \"1576675\": {\"node_is_primary\": false, \"name\": \"H2O\", \"label_x\": 4210.0, \"node_type\": \"metabolite\", \"y\": 3558.0, \"x\": 4192.0, \"bigg_id\": \"h2o_c\", \"label_y\": 3530.0}, \"1576674\": {\"node_is_primary\": false, \"name\": \"H+\", \"label_x\": 4456.0, \"node_type\": \"metabolite\", \"y\": 3087.0, \"x\": 4456.0, \"bigg_id\": \"h_c\", \"label_y\": 3109.0}, \"1576673\": {\"node_is_primary\": false, \"name\": \"H+\", \"label_x\": 4836.0, \"node_type\": \"metabolite\", \"y\": 3087.0, \"x\": 4836.0, \"bigg_id\": \"h_e\", \"label_y\": 3115.0}, \"1576672\": {\"node_is_primary\": false, \"name\": \"H+\", \"label_x\": 1169.0, \"node_type\": \"metabolite\", \"y\": 2465.0, \"x\": 1145.0, \"bigg_id\": \"h_c\", \"label_y\": 2465.0}, \"1576671\": {\"node_is_primary\": false, \"name\": \"Nicotinamide adenine dinucleotide - reduced\", \"label_x\": 1165.0, \"node_type\": \"metabolite\", \"y\": 2515.0, \"x\": 1145.0, \"bigg_id\": \"nadh_c\", \"label_y\": 2515.0}, \"1576670\": {\"node_is_primary\": false, \"name\": \"Phosphate\", \"label_x\": 1165.0, \"node_type\": \"metabolite\", \"y\": 2315.0, \"x\": 1145.0, \"bigg_id\": \"pi_c\", \"label_y\": 2315.0}, \"1576679\": {\"node_is_primary\": false, \"name\": \"H+\", \"label_x\": 4050.0, \"node_type\": \"metabolite\", \"y\": 3558.0, \"x\": 4042.0, \"bigg_id\": \"h_c\", \"label_y\": 3530.0}, \"1576678\": {\"node_is_primary\": false, \"name\": \"Ammonium\", \"label_x\": 3856.820068359375, \"node_type\": \"metabolite\", \"y\": 3558.0, \"x\": 3942.0, \"bigg_id\": \"nh4_c\", \"label_y\": 3573.222900390625}, \"1576938\": {\"y\": 4158.0, \"x\": 4415.0, \"node_type\": \"multimarker\"}, \"1576939\": {\"y\": 4158.0, \"x\": 4435.0, \"node_type\": \"midmarker\"}, \"1576930\": {\"y\": 1900.0, \"x\": 4630.0, \"node_type\": \"multimarker\"}, \"1576931\": {\"y\": 1425.0, \"x\": 2305.0, \"node_type\": \"midmarker\"}, \"1576932\": {\"y\": 3341.0, \"x\": 2843.0, \"node_type\": \"multimarker\"}, \"1576933\": {\"y\": 3445.0, \"x\": 2994.0, \"node_type\": \"multimarker\"}, \"1576934\": {\"y\": 3388.0, \"x\": 2911.0, \"node_type\": \"midmarker\"}, \"1576935\": {\"y\": 4065.0, \"x\": 1715.0, \"node_type\": \"multimarker\"}, \"1576936\": {\"y\": 4105.0, \"x\": 1715.0, \"node_type\": \"multimarker\"}, \"1576937\": {\"y\": 4085.0, \"x\": 1715.0, \"node_type\": \"midmarker\"}, \"1577009\": {\"y\": 470.0, \"x\": 2840.0, \"node_type\": \"midmarker\"}, \"1577008\": {\"y\": 2395.0, \"x\": 1055.0, \"node_type\": \"midmarker\"}, \"1577007\": {\"y\": 2435.0, \"x\": 1055.0, \"node_type\": \"multimarker\"}, \"1577006\": {\"y\": 2345.0, \"x\": 1055.0, \"node_type\": \"multimarker\"}, \"1577005\": {\"y\": 1265.0, \"x\": 1617.0, \"node_type\": \"multimarker\"}, \"1577004\": {\"y\": 1265.0, \"x\": 1657.0, \"node_type\": \"multimarker\"}, \"1577003\": {\"y\": 1265.0, \"x\": 1637.0, \"node_type\": \"midmarker\"}, \"1577002\": {\"y\": 2846.0, \"x\": 4560.0, \"node_type\": \"multimarker\"}, \"1577001\": {\"y\": 2846.0, \"x\": 4770.0, \"node_type\": \"multimarker\"}, \"1577000\": {\"y\": 2846.0, \"x\": 4667.0, \"node_type\": \"midmarker\"}, \"1577094\": {\"node_is_primary\": false, \"name\": \"Phosphoenolpyruvate\", \"label_x\": 4379.6201171875, \"node_type\": \"metabolite\", \"y\": 4629.59423828125, \"x\": 4414.0517578125, \"bigg_id\": \"pep_c\", \"label_y\": 4607.90673828125}, \"1577095\": {\"node_is_primary\": false, \"name\": \"L-Glutamine\", \"label_x\": 4253.14453125, \"node_type\": \"metabolite\", \"y\": 4650.7197265625, \"x\": 4332.9951171875, \"bigg_id\": \"gln__L_c\", \"label_y\": 4627.97607421875}, \"1577096\": {\"node_is_primary\": false, \"name\": \"Nicotinamide adenine dinucleotide phosphate\", \"label_x\": 4968.08447265625, \"node_type\": \"metabolite\", \"y\": 4864.818359375, \"x\": 5028.71435546875, \"bigg_id\": \"nadp_c\", \"label_y\": 4902.36181640625}, \"1577097\": {\"node_is_primary\": false, \"name\": \"Nicotinamide adenine dinucleotide phosphate - reduced\", \"label_x\": 4203.775390625, \"node_type\": \"metabolite\", \"y\": 4682.4072265625, \"x\": 4267.78271484375, \"bigg_id\": \"nadph_c\", \"label_y\": 4664.94482421875}, \"1577090\": {\"node_is_primary\": false, \"name\": \"Nicotinamide adenine dinucleotide - reduced\", \"label_x\": 5078.88623046875, \"node_type\": \"metabolite\", \"y\": 4897.1259765625, \"x\": 5134.0517578125, \"bigg_id\": \"nadh_c\", \"label_y\": 4936.322265625}, \"1577091\": {\"node_is_primary\": false, \"name\": \"2-Oxoglutarate\", \"label_x\": 5497.31591796875, \"node_type\": \"metabolite\", \"y\": 4979.51025390625, \"x\": 5481.046875, \"bigg_id\": \"akg_c\", \"label_y\": 5004.2978515625}, \"1577092\": {\"node_is_primary\": false, \"name\": \"Acetyl-CoA\", \"label_x\": 4556.52001953125, \"node_type\": \"metabolite\", \"y\": 4576.78173828125, \"x\": 4601.51416015625, \"bigg_id\": \"accoa_c\", \"label_y\": 4549.81298828125}, \"1577093\": {\"node_is_primary\": false, \"name\": \"3-Phospho-D-glycerate\", \"label_x\": 4459.6201171875, \"node_type\": \"metabolite\", \"y\": 4602.1318359375, \"x\": 4508.83935546875, \"bigg_id\": \"3pg_c\", \"label_y\": 4576.21923828125}, \"1577098\": {\"node_is_primary\": false, \"name\": \"Phosphate\", \"label_x\": 4884.8583984375, \"node_type\": \"metabolite\", \"y\": 4831.37548828125, \"x\": 4933.43505859375, \"bigg_id\": \"pi_c\", \"label_y\": 4859.1142578125}, \"1577099\": {\"y\": 4023.6457547875243, \"x\": 3055.6239109998846, \"node_type\": \"multimarker\"}, \"1576529\": {\"node_is_primary\": true, \"name\": \"D-Fructose 1,6-bisphosphate\", \"label_x\": 1096.404296875, \"node_type\": \"metabolite\", \"y\": 1925.0, \"x\": 1055.0, \"bigg_id\": \"fdp_c\", \"label_y\": 1928.173583984375}, \"1576528\": {\"node_is_primary\": false, \"name\": \"H2O\", \"label_x\": 672.9427490234375, \"node_type\": \"metabolite\", \"y\": 1855.0, \"x\": 765.0, \"bigg_id\": \"h2o_c\", \"label_y\": 1861.3470458984375}, \"1576527\": {\"node_is_primary\": true, \"name\": \"Ammonium\", \"label_x\": 4019.0, \"node_type\": \"metabolite\", \"y\": 4441.0, \"x\": 3989.0, \"bigg_id\": \"nh4_c\", \"label_y\": 4441.0}, \"1576526\": {\"node_is_primary\": true, \"name\": \"Ammonium\", \"label_x\": 4019.0, \"node_type\": \"metabolite\", \"y\": 4757.0, \"x\": 3989.0, \"bigg_id\": \"nh4_e\", \"label_y\": 4757.0}, \"1576525\": {\"node_is_primary\": true, \"name\": \"D-Fructose 6-phosphate\", \"label_x\": 1075.0, \"node_type\": \"metabolite\", \"y\": 1545.0, \"x\": 1055.0, \"bigg_id\": \"f6p_c\", \"label_y\": 1525.0}, \"1576524\": {\"node_is_primary\": true, \"name\": \"D-Glucose 6-phosphate\", \"label_x\": 1075.0, \"node_type\": \"metabolite\", \"y\": 1265.0, \"x\": 1055.0, \"bigg_id\": \"g6p_c\", \"label_y\": 1245.0}, \"1576523\": {\"node_is_primary\": false, \"name\": \"H2O\", \"label_x\": 886.289794921875, \"node_type\": \"metabolite\", \"y\": 3325.0, \"x\": 965.0, \"bigg_id\": \"h2o_c\", \"label_y\": 3307.958740234375}, \"1576522\": {\"node_is_primary\": true, \"name\": \"D-Glycerate 2-phosphate\", \"label_x\": 1093.0, \"node_type\": \"metabolite\", \"y\": 3125.0, \"x\": 1055.0, \"bigg_id\": \"2pg_c\", \"label_y\": 3125.0}, \"1576521\": {\"node_is_primary\": true, \"name\": \"Oxaloacetate\", \"label_x\": 2692.0, \"node_type\": \"metabolite\", \"y\": 3724.0, \"x\": 2659.0, \"bigg_id\": \"oaa_c\", \"label_y\": 3724.0}, \"1576520\": {\"node_is_primary\": false, \"name\": \"Phosphate\", \"label_x\": 1685.0, \"node_type\": \"metabolite\", \"y\": 3515.0, \"x\": 1665.0, \"bigg_id\": \"pi_c\", \"label_y\": 3515.0}, \"1576488\": {\"node_is_primary\": false, \"name\": \"ADP\", \"label_x\": 1165.0, \"node_type\": \"metabolite\", \"y\": 2635.0, \"x\": 1145.0, \"bigg_id\": \"adp_c\", \"label_y\": 2635.0}, \"1576841\": {\"y\": 820.0, \"x\": 3510.0, \"node_type\": \"midmarker\"}, \"1576842\": {\"y\": 800.0, \"x\": 3510.0, \"node_type\": \"multimarker\"}, \"1576843\": {\"y\": 870.0, \"x\": 3510.0, \"node_type\": \"multimarker\"}, \"1576844\": {\"y\": 3022.0, \"x\": 2727.0, \"node_type\": \"multimarker\"}, \"1576845\": {\"y\": 3076.0, \"x\": 2684.0, \"node_type\": \"midmarker\"}, \"1576846\": {\"y\": 3846.0, \"x\": 3606.0, \"node_type\": \"midmarker\"}, \"1576847\": {\"y\": 3923.0, \"x\": 3512.0, \"node_type\": \"multimarker\"}, \"1576848\": {\"y\": 3781.0, \"x\": 3663.0, \"node_type\": \"multimarker\"}, \"1576849\": {\"y\": 3945.0, \"x\": 1430.0, \"node_type\": \"multimarker\"}, \"1576485\": {\"node_is_primary\": false, \"name\": \"ATP\", \"label_x\": 1165.0, \"node_type\": \"metabolite\", \"y\": 2805.0, \"x\": 1145.0, \"bigg_id\": \"atp_c\", \"label_y\": 2805.0}, \"1576486\": {\"node_is_primary\": true, \"name\": \"3-Phospho-D-glycerate\", \"label_x\": 1085.0, \"node_type\": \"metabolite\", \"y\": 2875.0, \"x\": 1055.0, \"bigg_id\": \"3pg_c\", \"label_y\": 2875.0}, \"1576487\": {\"node_is_primary\": true, \"name\": \"3-Phospho-D-glyceroyl phosphate\", \"label_x\": 1085.0, \"node_type\": \"metabolite\", \"y\": 2565.0, \"x\": 1055.0, \"bigg_id\": \"13dpg_c\", \"label_y\": 2565.0}, \"1576558\": {\"node_is_primary\": true, \"name\": \"D-Xylulose 5-phosphate\", \"label_x\": 1944.76025390625, \"node_type\": \"metabolite\", \"y\": 1555.0, \"x\": 1945.0, \"bigg_id\": \"xu5p__D_c\", \"label_y\": 1599.5205078125}, \"1576559\": {\"node_is_primary\": true, \"name\": \"H+\", \"label_x\": 4867.7265625, \"node_type\": \"metabolite\", \"y\": 2229.0, \"x\": 4850.0, \"bigg_id\": \"h_e\", \"label_y\": 2273.503662109375}, \"1576552\": {\"node_is_primary\": true, \"name\": \"L-Glutamate\", \"label_x\": 4883.0, \"node_type\": \"metabolite\", \"y\": 3634.0, \"x\": 4883.0, \"bigg_id\": \"glu__L_e\", \"label_y\": 3604.0}, \"1576553\": {\"node_is_primary\": false, \"name\": \"H+\", \"label_x\": 4857.23046875, \"node_type\": \"metabolite\", \"y\": 3703.0, \"x\": 4825.0, \"bigg_id\": \"h_e\", \"label_y\": 3723.0}, \"1576550\": {\"node_is_primary\": false, \"name\": \"ADP\", \"label_x\": 903.744140625, \"node_type\": \"metabolite\", \"y\": 3555.0, \"x\": 965.0, \"bigg_id\": \"adp_c\", \"label_y\": 3528.02490234375}, \"1576551\": {\"node_is_primary\": false, \"name\": \"ATP\", \"label_x\": 929.1323852539062, \"node_type\": \"metabolite\", \"y\": 3815.0, \"x\": 965.0, \"bigg_id\": \"atp_c\", \"label_y\": 3794.372314453125}, \"1576556\": {\"node_is_primary\": true, \"name\": \"D-Lactate\", \"label_x\": 1088.470458984375, \"node_type\": \"metabolite\", \"y\": 4835.0, \"x\": 1055.0, \"bigg_id\": \"lac__D_e\", \"label_y\": 4857.21484375}, \"1576557\": {\"node_is_primary\": true, \"name\": \"Pyruvate\", \"label_x\": 249.61181640625, \"node_type\": \"metabolite\", \"y\": 3945.0, \"x\": 275.0, \"bigg_id\": \"pyr_e\", \"label_y\": 3994.041015625}, \"1576554\": {\"node_is_primary\": true, \"name\": \"L-Glutamate\", \"label_x\": 4221.2158203125, \"node_type\": \"metabolite\", \"y\": 3632.0, \"x\": 4248.0, \"bigg_id\": \"glu__L_c\", \"label_y\": 3601.0}, \"1576555\": {\"node_is_primary\": false, \"name\": \"H+\", \"label_x\": 4503.1796875, \"node_type\": \"metabolite\", \"y\": 3688.0, \"x\": 4478.0, \"bigg_id\": \"h_c\", \"label_y\": 3696.834716796875}, \"1576639\": {\"node_is_primary\": true, \"name\": \"H2O\", \"label_x\": 3287.0, \"node_type\": \"metabolite\", \"y\": 4754.0, \"x\": 3257.0, \"bigg_id\": \"h2o_e\", \"label_y\": 4754.0}, \"1576638\": {\"node_is_primary\": false, \"name\": \"H+\", \"label_x\": 2692.0, \"node_type\": \"metabolite\", \"y\": 3510.0, \"x\": 2660.0, \"bigg_id\": \"h_c\", \"label_y\": 3510.0}, \"1576633\": {\"node_is_primary\": false, \"name\": \"Nicotinamide adenine dinucleotide\", \"label_x\": 935.0, \"node_type\": \"metabolite\", \"y\": 4255.0, \"x\": 955.0, \"bigg_id\": \"nad_c\", \"label_y\": 4255.0}, \"1576632\": {\"node_is_primary\": false, \"name\": \"H+\", \"label_x\": 4305.0, \"node_type\": \"metabolite\", \"y\": 1380.0, \"x\": 4285.0, \"bigg_id\": \"h_c\", \"label_y\": 1380.0}, \"1576631\": {\"node_is_primary\": false, \"name\": \"Phosphate\", \"label_x\": 4305.0, \"node_type\": \"metabolite\", \"y\": 1450.0, \"x\": 4285.0, \"bigg_id\": \"pi_c\", \"label_y\": 1450.0}, \"1576630\": {\"node_is_primary\": false, \"name\": \"H2O\", \"label_x\": 4295.0, \"node_type\": \"metabolite\", \"y\": 1140.0, \"x\": 4275.0, \"bigg_id\": \"h2o_c\", \"label_y\": 1140.0}, \"1576637\": {\"node_is_primary\": false, \"name\": \"Nicotinamide adenine dinucleotide - reduced\", \"label_x\": 2710.0, \"node_type\": \"metabolite\", \"y\": 3585.0, \"x\": 2680.0, \"bigg_id\": \"nadh_c\", \"label_y\": 3590.0}, \"1576636\": {\"node_is_primary\": false, \"name\": \"Nicotinamide adenine dinucleotide\", \"label_x\": 2690.0, \"node_type\": \"metabolite\", \"y\": 3310.0, \"x\": 2670.0, \"bigg_id\": \"nad_c\", \"label_y\": 3320.0}, \"1576635\": {\"node_is_primary\": false, \"name\": \"H+\", \"label_x\": 935.0, \"node_type\": \"metabolite\", \"y\": 4095.0, \"x\": 955.0, \"bigg_id\": \"h_c\", \"label_y\": 4095.0}, \"1576634\": {\"node_is_primary\": false, \"name\": \"Nicotinamide adenine dinucleotide - reduced\", \"label_x\": 935.0, \"node_type\": \"metabolite\", \"y\": 4045.0, \"x\": 955.0, \"bigg_id\": \"nadh_c\", \"label_y\": 4045.0}, \"1577043\": {\"y\": 4071.0, \"x\": 2342.0, \"node_type\": \"multimarker\"}, \"1577042\": {\"y\": 4086.0, \"x\": 2356.0, \"node_type\": \"midmarker\"}, \"1577041\": {\"y\": 4100.0, \"x\": 2371.0, \"node_type\": \"multimarker\"}, \"1577047\": {\"y\": 4432.0, \"x\": 1460.0, \"node_type\": \"midmarker\"}, \"1577046\": {\"y\": 4470.0, \"x\": 1360.0, \"node_type\": \"multimarker\"}, \"1577045\": {\"y\": 4600.0, \"x\": 1360.0, \"node_type\": \"midmarker\"}, \"1577044\": {\"y\": 4730.0, \"x\": 1360.0, \"node_type\": \"multimarker\"}, \"1577049\": {\"y\": 2600.0, \"x\": 3110.0, \"node_type\": \"midmarker\"}, \"1577048\": {\"y\": 2600.0, \"x\": 3080.0, \"node_type\": \"multimarker\"}, \"1576941\": {\"y\": 3330.0, \"x\": 2310.0, \"node_type\": \"multimarker\"}, \"1576940\": {\"y\": 4158.0, \"x\": 4455.0, \"node_type\": \"multimarker\"}, \"1576943\": {\"y\": 3390.0, \"x\": 2190.0, \"node_type\": \"multimarker\"}, \"1576942\": {\"y\": 3360.0, \"x\": 2250.0, \"node_type\": \"midmarker\"}, \"1576945\": {\"y\": 1265.0, \"x\": 1307.0, \"node_type\": \"midmarker\"}, \"1576944\": {\"y\": 1265.0, \"x\": 1327.0, \"node_type\": \"multimarker\"}, \"1576947\": {\"y\": 4935.0, \"x\": 1715.0, \"node_type\": \"midmarker\"}, \"1576946\": {\"y\": 1265.0, \"x\": 1287.0, \"node_type\": \"multimarker\"}, \"1576949\": {\"y\": 3815.0, \"x\": 4247.0, \"node_type\": \"multimarker\"}, \"1576628\": {\"node_is_primary\": false, \"name\": \"H2O\", \"label_x\": 4175.82763671875, \"node_type\": \"metabolite\", \"y\": 3940.0, \"x\": 4201.0, \"bigg_id\": \"h2o_c\", \"label_y\": 3972.89208984375}, \"1576629\": {\"node_is_primary\": false, \"name\": \"Ammonium\", \"label_x\": 4160.99267578125, \"node_type\": \"metabolite\", \"y\": 3762.0, \"x\": 4193.0, \"bigg_id\": \"nh4_c\", \"label_y\": 3793.22314453125}, \"1576888\": {\"y\": 3020.0, \"x\": 3631.0, \"node_type\": \"midmarker\"}, \"1576889\": {\"y\": 2976.0, \"x\": 3588.0, \"node_type\": \"multimarker\"}, \"1576983\": {\"y\": 4255.0, \"x\": 1715.0, \"node_type\": \"multimarker\"}, \"1576620\": {\"node_is_primary\": false, \"name\": \"Nicotinamide adenine dinucleotide\", \"label_x\": 2380.0, \"node_type\": \"metabolite\", \"y\": 3220.0, \"x\": 2360.0, \"bigg_id\": \"nad_c\", \"label_y\": 3220.0}, \"1576621\": {\"node_is_primary\": false, \"name\": \"CO2\", \"label_x\": 1979.703125, \"node_type\": \"metabolite\", \"y\": 3420.0, \"x\": 2060.0, \"bigg_id\": \"co2_c\", \"label_y\": 3415.23974609375}, \"1576839\": {\"y\": 1265.0, \"x\": 1977.0, \"node_type\": \"multimarker\"}, \"1576838\": {\"y\": 1265.0, \"x\": 1937.0, \"node_type\": \"multimarker\"}, \"1576831\": {\"node_is_primary\": false, \"name\": \"H+\", \"label_x\": 2460.1826171875, \"node_type\": \"metabolite\", \"y\": 700.0, \"x\": 2510.0, \"bigg_id\": \"h_e\", \"label_y\": 722.5455932617188}, \"1576830\": {\"node_is_primary\": false, \"name\": \"Ubiquinol-8\", \"label_x\": 3041.5869140625, \"node_type\": \"metabolite\", \"y\": 2740.0, \"x\": 3020.0, \"bigg_id\": \"q8h2_c\", \"label_y\": 2740.0}, \"1576833\": {\"node_is_primary\": false, \"name\": \"H+\", \"label_x\": 1228.8927001953125, \"node_type\": \"metabolite\", \"y\": 4770.0, \"x\": 1270.0, \"bigg_id\": \"h_e\", \"label_y\": 4750.95849609375}, \"1576832\": {\"node_is_primary\": false, \"name\": \"H+\", \"label_x\": 1950.0, \"node_type\": \"metabolite\", \"y\": 3860.0, \"x\": 1950.0, \"bigg_id\": \"h_c\", \"label_y\": 3840.0}, \"1576835\": {\"y\": 2775.0, \"x\": 1055.0, \"node_type\": \"multimarker\"}, \"1576834\": {\"y\": 2665.0, \"x\": 1055.0, \"node_type\": \"multimarker\"}, \"1576837\": {\"y\": 1265.0, \"x\": 1957.0, \"node_type\": \"midmarker\"}, \"1576836\": {\"y\": 2725.0, \"x\": 1055.0, \"node_type\": \"midmarker\"}, \"1576688\": {\"node_is_primary\": false, \"name\": \"H+\", \"label_x\": 351.3470764160156, \"node_type\": \"metabolite\", \"y\": 4045.0, \"x\": 345.0, \"bigg_id\": \"h_e\", \"label_y\": 4072.933837890625}, \"1576689\": {\"node_is_primary\": false, \"name\": \"H+\", \"label_x\": 675.0, \"node_type\": \"metabolite\", \"y\": 4045.0, \"x\": 675.0, \"bigg_id\": \"h_c\", \"label_y\": 4077.694091796875}, \"1576567\": {\"node_is_primary\": false, \"name\": \"Coenzyme A\", \"label_x\": 3448.892578125, \"node_type\": \"metabolite\", \"y\": 2960.0, \"x\": 3460.0, \"bigg_id\": \"coa_c\", \"label_y\": 2991.107421875}, \"1576682\": {\"node_is_primary\": false, \"name\": \"H+\", \"label_x\": 4398.884765625, \"node_type\": \"metabolite\", \"y\": 1150.0, \"x\": 4415.0, \"bigg_id\": \"h_c\", \"label_y\": 1187.7266845703125}, \"1576683\": {\"node_is_primary\": false, \"name\": \"H2O\", \"label_x\": 4482.7265625, \"node_type\": \"metabolite\", \"y\": 1030.0, \"x\": 4482.7265625, \"bigg_id\": \"h2o_c\", \"label_y\": 1010.0}, \"1576680\": {\"node_is_primary\": false, \"name\": \"H+\", \"label_x\": 4865.0, \"node_type\": \"metabolite\", \"y\": 1150.0, \"x\": 4865.0, \"bigg_id\": \"h_e\", \"label_y\": 1180.0}, \"1576568\": {\"node_is_primary\": false, \"name\": \"ATP\", \"label_x\": 3496.826416015625, \"node_type\": \"metabolite\", \"y\": 3030.0, \"x\": 3500.0, \"bigg_id\": \"atp_c\", \"label_y\": 3062.694091796875}, \"1576686\": {\"node_is_primary\": false, \"name\": \"Coenzyme A\", \"label_x\": 2786.826416015625, \"node_type\": \"metabolite\", \"y\": 4091.0, \"x\": 2806.0, \"bigg_id\": \"coa_c\", \"label_y\": 4132.21484375}, \"1576687\": {\"node_is_primary\": false, \"name\": \"H+\", \"label_x\": 2899.041015625, \"node_type\": \"metabolite\", \"y\": 4117.0, \"x\": 2897.0, \"bigg_id\": \"h_c\", \"label_y\": 4155.8671875}, \"1576684\": {\"node_is_primary\": false, \"name\": \"H2O\", \"label_x\": 2599.520751953125, \"node_type\": \"metabolite\", \"y\": 3860.0, \"x\": 2610.0, \"bigg_id\": \"h2o_c\", \"label_y\": 3890.1484375}, \"1576685\": {\"node_is_primary\": true, \"name\": \"Citrate\", \"label_x\": 2972.653076171875, \"node_type\": \"metabolite\", \"y\": 3986.0, \"x\": 2971.0, \"bigg_id\": \"cit_c\", \"label_y\": 3943.1826171875}, \"1576664\": {\"node_is_primary\": false, \"name\": \"H+\", \"label_x\": 4818.7197265625, \"node_type\": \"metabolite\", \"y\": 2921.942138671875, \"x\": 4791.7197265625, \"bigg_id\": \"h_e\", \"label_y\": 2921.942138671875}, \"1576665\": {\"node_is_primary\": false, \"name\": \"H+\", \"label_x\": 4389.423828125, \"node_type\": \"metabolite\", \"y\": 2763.553955078125, \"x\": 4461.380859375, \"bigg_id\": \"h_c\", \"label_y\": 2763.9423828125}, \"1576666\": {\"node_is_primary\": true, \"name\": \"Formate\", \"label_x\": 1500.0, \"node_type\": \"metabolite\", \"y\": 4843.322265625, \"x\": 1460.0, \"bigg_id\": \"for_e\", \"label_y\": 4843.322265625}, \"1576667\": {\"node_is_primary\": false, \"name\": \"H2O\", \"label_x\": 1587.0, \"node_type\": \"metabolite\", \"y\": 1160.0, \"x\": 1587.0, \"bigg_id\": \"h2o_c\", \"label_y\": 1140.0}, \"1576660\": {\"node_is_primary\": false, \"name\": \"H+\", \"label_x\": 1835.0, \"node_type\": \"metabolite\", \"y\": 4775.0, \"x\": 1815.0, \"bigg_id\": \"h_e\", \"label_y\": 4775.0}, \"1576661\": {\"node_is_primary\": false, \"name\": \"H+\", \"label_x\": 1839.76025390625, \"node_type\": \"metabolite\", \"y\": 4452.45458984375, \"x\": 1819.76025390625, \"bigg_id\": \"h_c\", \"label_y\": 4452.45458984375}, \"1576662\": {\"node_is_primary\": false, \"name\": \"Phosphoenolpyruvate\", \"label_x\": 877.8765258789062, \"node_type\": \"metabolite\", \"y\": 974.4060668945312, \"x\": 957.694091796875, \"bigg_id\": \"pep_c\", \"label_y\": 976.9517211914062}, \"1576663\": {\"node_is_primary\": false, \"name\": \"Pyruvate\", \"label_x\": 1193.082275390625, \"node_type\": \"metabolite\", \"y\": 972.8192138671875, \"x\": 1161.3470458984375, \"bigg_id\": \"pyr_c\", \"label_y\": 986.47216796875}, \"1576668\": {\"node_is_primary\": false, \"name\": \"H+\", \"label_x\": 1687.0, \"node_type\": \"metabolite\", \"y\": 1160.0, \"x\": 1687.0, \"bigg_id\": \"h_c\", \"label_y\": 1140.0}, \"1576669\": {\"node_is_primary\": false, \"name\": \"Nicotinamide adenine dinucleotide\", \"label_x\": 1165.0, \"node_type\": \"metabolite\", \"y\": 2265.0, \"x\": 1145.0, \"bigg_id\": \"nad_c\", \"label_y\": 2265.0}, \"1576905\": {\"y\": 1270.0, \"x\": 3470.0, \"node_type\": \"multimarker\"}, \"1576907\": {\"y\": 1270.0, \"x\": 3520.0, \"node_type\": \"midmarker\"}, \"1576906\": {\"y\": 1270.0, \"x\": 3570.0, \"node_type\": \"multimarker\"}, \"1576901\": {\"y\": 3849.0, \"x\": 4364.0, \"node_type\": \"multimarker\"}, \"1576900\": {\"y\": 3869.0, \"x\": 4364.0, \"node_type\": \"midmarker\"}, \"1576909\": {\"y\": 3402.0, \"x\": 3783.0, \"node_type\": \"midmarker\"}, \"1576908\": {\"y\": 3480.0, \"x\": 3780.0, \"node_type\": \"multimarker\"}, \"1577018\": {\"y\": 1150.0, \"x\": 4625.0, \"node_type\": \"multimarker\"}, \"1577019\": {\"y\": 1150.0, \"x\": 4665.0, \"node_type\": \"midmarker\"}, \"1577014\": {\"y\": 3632.0, \"x\": 4078.0, \"node_type\": \"multimarker\"}, \"1577015\": {\"y\": 3632.0, \"x\": 4098.0, \"node_type\": \"midmarker\"}, \"1577016\": {\"y\": 3632.0, \"x\": 4118.0, \"node_type\": \"multimarker\"}, \"1577017\": {\"y\": 1150.0, \"x\": 4705.0, \"node_type\": \"multimarker\"}, \"1577011\": {\"y\": 3027.0, \"x\": 4666.0, \"node_type\": \"midmarker\"}, \"1577012\": {\"y\": 3027.0, \"x\": 4536.0, \"node_type\": \"multimarker\"}, \"1577013\": {\"y\": 3027.0, \"x\": 4796.0, \"node_type\": \"multimarker\"}, \"1576840\": {\"y\": 1660.0, \"x\": 4660.0, \"node_type\": \"midmarker\"}, \"1576489\": {\"node_is_primary\": false, \"name\": \"Nicotinamide adenine dinucleotide phosphate\", \"label_x\": 1907.0, \"node_type\": \"metabolite\", \"y\": 1165.0, \"x\": 1907.0, \"bigg_id\": \"nadp_c\", \"label_y\": 1145.0}, \"1576539\": {\"node_is_primary\": true, \"name\": \"Ubiquinone-8\", \"label_x\": 4686.61181640625, \"node_type\": \"metabolite\", \"y\": 2215.0361328125, \"x\": 4758.67626953125, \"bigg_id\": \"q8_c\", \"label_y\": 2192.47509765625}, \"1576534\": {\"node_is_primary\": true, \"name\": \"ATP\", \"label_x\": 4215.0, \"node_type\": \"metabolite\", \"y\": 1040.0, \"x\": 4215.0, \"bigg_id\": \"atp_c\", \"label_y\": 1010.0}, \"1576535\": {\"node_is_primary\": true, \"name\": \"AMP\", \"label_x\": 4033.388427734375, \"node_type\": \"metabolite\", \"y\": 1070.0, \"x\": 4065.0, \"bigg_id\": \"amp_c\", \"label_y\": 1030.661865234375}, \"1576536\": {\"node_is_primary\": true, \"name\": \"ADP\", \"label_x\": 4196.611328125, \"node_type\": \"metabolite\", \"y\": 1510.0, \"x\": 4215.0, \"bigg_id\": \"adp_c\", \"label_y\": 1556.1151123046875}, \"1576537\": {\"node_is_primary\": true, \"name\": \"L-Glutamine\", \"label_x\": 4851.38134765625, \"node_type\": \"metabolite\", \"y\": 4158.0, \"x\": 4882.0, \"bigg_id\": \"gln__L_e\", \"label_y\": 4199.2802734375}, \"1576530\": {\"node_is_primary\": false, \"name\": \"Phosphate\", \"label_x\": 686.744140625, \"node_type\": \"metabolite\", \"y\": 1565.0, \"x\": 755.0, \"bigg_id\": \"pi_c\", \"label_y\": 1563.4132080078125}, \"1576531\": {\"node_is_primary\": true, \"name\": \"Succinate\", \"label_x\": 4950.4462890625, \"node_type\": \"metabolite\", \"y\": 2880.0, \"x\": 4932.0, \"bigg_id\": \"succ_e\", \"label_y\": 2915.280517578125}, \"1576532\": {\"node_is_primary\": true, \"name\": \"Glyoxylate\", \"label_x\": 3211.3271484375, \"node_type\": \"metabolite\", \"y\": 3570.0, \"x\": 3180.0, \"bigg_id\": \"glx_c\", \"label_y\": 3543.267578125}, \"1576533\": {\"node_is_primary\": true, \"name\": \"Succinate\", \"label_x\": 3439.01611328125, \"node_type\": \"metabolite\", \"y\": 2867.0, \"x\": 3420.0, \"bigg_id\": \"succ_c\", \"label_y\": 2838.810302734375}, \"1576879\": {\"y\": 3635.0, \"x\": 4541.0, \"node_type\": \"multimarker\"}, \"1576878\": {\"y\": 3635.0, \"x\": 4645.0, \"node_type\": \"midmarker\"}, \"1576875\": {\"y\": 3655.0, \"x\": 1055.0, \"node_type\": \"multimarker\"}, \"1576874\": {\"y\": 2035.0, \"x\": 2178.0, \"node_type\": \"multimarker\"}, \"1576877\": {\"y\": 3745.0, \"x\": 1055.0, \"node_type\": \"multimarker\"}, \"1576876\": {\"y\": 3705.0, \"x\": 1055.0, \"node_type\": \"midmarker\"}, \"1576871\": {\"y\": 4914.0, \"x\": 3621.0, \"node_type\": \"midmarker\"}, \"1576569\": {\"node_is_primary\": false, \"name\": \"Phosphate\", \"label_x\": 3814.0, \"node_type\": \"metabolite\", \"y\": 3051.0, \"x\": 3794.0, \"bigg_id\": \"pi_c\", \"label_y\": 3051.0}, \"1576873\": {\"y\": 1955.0, \"x\": 2175.0, \"node_type\": \"multimarker\"}, \"1576872\": {\"y\": 1995.0, \"x\": 2178.0, \"node_type\": \"midmarker\"}, \"1576681\": {\"node_is_primary\": false, \"name\": \"Phosphate\", \"label_x\": 4459.546875, \"node_type\": \"metabolite\", \"y\": 1330.0, \"x\": 4495.0, \"bigg_id\": \"pi_c\", \"label_y\": 1293.5970458984375}, \"1576581\": {\"node_is_primary\": false, \"name\": \"Phosphate\", \"label_x\": 4454.0, \"node_type\": \"metabolite\", \"y\": 4054.0, \"x\": 4434.0, \"bigg_id\": \"pi_c\", \"label_y\": 4054.0}, \"1576580\": {\"node_is_primary\": false, \"name\": \"H+\", \"label_x\": 4454.0, \"node_type\": \"metabolite\", \"y\": 4004.0, \"x\": 4434.0, \"bigg_id\": \"h_c\", \"label_y\": 4004.0}, \"1576585\": {\"node_is_primary\": false, \"name\": \"Coenzyme A\", \"label_x\": 3628.99267578125, \"node_type\": \"metabolite\", \"y\": 3506.0, \"x\": 3684.0, \"bigg_id\": \"coa_c\", \"label_y\": 3483.05029296875}, \"1576584\": {\"node_is_primary\": true, \"name\": \"D-Lactate\", \"label_x\": 1089.0, \"node_type\": \"metabolite\", \"y\": 4345.0, \"x\": 1055.0, \"bigg_id\": \"lac__D_c\", \"label_y\": 4349.0}, \"1576587\": {\"node_is_primary\": false, \"name\": \"CO2\", \"label_x\": 3898.0, \"node_type\": \"metabolite\", \"y\": 3285.0, \"x\": 3872.0, \"bigg_id\": \"co2_c\", \"label_y\": 3285.0}, \"1576586\": {\"node_is_primary\": false, \"name\": \"Nicotinamide adenine dinucleotide\", \"label_x\": 3606.208740234375, \"node_type\": \"metabolite\", \"y\": 3558.0, \"x\": 3684.0, \"bigg_id\": \"nad_c\", \"label_y\": 3539.885009765625}, \"1576589\": {\"node_is_primary\": false, \"name\": \"Coenzyme A\", \"label_x\": 1165.0, \"node_type\": \"metabolite\", \"y\": 4045.0, \"x\": 1165.0, \"bigg_id\": \"coa_c\", \"label_y\": 4065.0}, \"1576588\": {\"node_is_primary\": false, \"name\": \"Nicotinamide adenine dinucleotide - reduced\", \"label_x\": 3846.0, \"node_type\": \"metabolite\", \"y\": 3233.0, \"x\": 3820.0, \"bigg_id\": \"nadh_c\", \"label_y\": 3233.0}, \"1576622\": {\"node_is_primary\": false, \"name\": \"Nicotinamide adenine dinucleotide - reduced\", \"label_x\": 1994.9427490234375, \"node_type\": \"metabolite\", \"y\": 3370.0, \"x\": 2080.0, \"bigg_id\": \"nadh_c\", \"label_y\": 3350.958740234375}, \"1576623\": {\"node_is_primary\": false, \"name\": \"Nicotinamide adenine dinucleotide phosphate\", \"label_x\": 1257.0, \"node_type\": \"metabolite\", \"y\": 1160.0, \"x\": 1257.0, \"bigg_id\": \"nadp_c\", \"label_y\": 1140.0}, \"1576624\": {\"node_is_primary\": false, \"name\": \"Nicotinamide adenine dinucleotide phosphate - reduced\", \"label_x\": 1430.0, \"node_type\": \"metabolite\", \"y\": 1160.0, \"x\": 1407.0, \"bigg_id\": \"nadph_c\", \"label_y\": 1140.0}, \"1576625\": {\"node_is_primary\": false, \"name\": \"H+\", \"label_x\": 1357.0, \"node_type\": \"metabolite\", \"y\": 1160.0, \"x\": 1357.0, \"bigg_id\": \"h_c\", \"label_y\": 1140.0}, \"1576626\": {\"node_is_primary\": true, \"name\": \"6-phospho-D-glucono-1,5-lactone\", \"label_x\": 1507.0, \"node_type\": \"metabolite\", \"y\": 1265.0, \"x\": 1507.0, \"bigg_id\": \"6pgl_c\", \"label_y\": 1235.0}, \"1576627\": {\"node_is_primary\": true, \"name\": \"Acetate\", \"label_x\": 1759.0985107421875, \"node_type\": \"metabolite\", \"y\": 4845.0, \"x\": 1715.0, \"bigg_id\": \"ac_e\", \"label_y\": 4848.173828125}, \"1577050\": {\"y\": 2600.0, \"x\": 3140.0, \"node_type\": \"multimarker\"}, \"1577051\": {\"y\": 1361.5867919921875, \"x\": 576.050048828125, \"node_type\": \"multimarker\"}, \"1577052\": {\"y\": 1361.586669921875, \"x\": 640.0, \"node_type\": \"midmarker\"}, \"1577053\": {\"y\": 1361.586669921875, \"x\": 704.5778198242188, \"node_type\": \"multimarker\"}, \"1577054\": {\"y\": 1360.0, \"x\": 182.0, \"node_type\": \"midmarker\"}, \"1577056\": {\"y\": 736.0, \"x\": 2610.0, \"node_type\": \"multimarker\"}, \"1577057\": {\"y\": 756.0, \"x\": 2610.0, \"node_type\": \"midmarker\"}, \"1577058\": {\"y\": 980.0, \"x\": 2610.0, \"node_type\": \"multimarker\"}, \"1577059\": {\"y\": 470.0, \"x\": 2610.0, \"node_type\": \"midmarker\"}, \"1576897\": {\"y\": 1735.0, \"x\": 1695.0, \"node_type\": \"multimarker\"}, \"1576896\": {\"y\": 1735.0, \"x\": 1525.0, \"node_type\": \"midmarker\"}, \"1576895\": {\"y\": 3636.0, \"x\": 5015.0, \"node_type\": \"midmarker\"}, \"1576893\": {\"y\": 1690.0, \"x\": 1055.0, \"node_type\": \"multimarker\"}, \"1576892\": {\"y\": 1775.0, \"x\": 1055.0, \"node_type\": \"multimarker\"}, \"1576891\": {\"y\": 1735.0, \"x\": 1055.0, \"node_type\": \"midmarker\"}, \"1576890\": {\"y\": 3063.0, \"x\": 3666.0, \"node_type\": \"multimarker\"}, \"1576956\": {\"y\": 4225.0, \"x\": 1055.0, \"node_type\": \"multimarker\"}, \"1576957\": {\"y\": 4175.0, \"x\": 1055.0, \"node_type\": \"midmarker\"}, \"1576954\": {\"y\": 1350.0, \"x\": 4215.0, \"node_type\": \"multimarker\"}, \"1576955\": {\"y\": 4125.0, \"x\": 1055.0, \"node_type\": \"multimarker\"}, \"1576952\": {\"y\": 1200.0, \"x\": 4215.0, \"node_type\": \"multimarker\"}, \"1576953\": {\"y\": 1260.0, \"x\": 4215.0, \"node_type\": \"midmarker\"}, \"1576899\": {\"y\": 3889.0, \"x\": 4364.0, \"node_type\": \"multimarker\"}, \"1576898\": {\"y\": 1735.0, \"x\": 1405.0, \"node_type\": \"multimarker\"}, \"1576958\": {\"y\": 3456.0, \"x\": 2578.0, \"node_type\": \"multimarker\"}, \"1576959\": {\"y\": 3404.0, \"x\": 2577.0, \"node_type\": \"midmarker\"}, \"1576950\": {\"y\": 3877.0, \"x\": 4247.0, \"node_type\": \"multimarker\"}, \"1576951\": {\"y\": 3857.0, \"x\": 4247.0, \"node_type\": \"midmarker\"}, \"1576826\": {\"node_is_primary\": true, \"name\": \"D-Fructose\", \"label_x\": 284.1323547363281, \"node_type\": \"metabolite\", \"y\": 1360.0, \"x\": 300.0, \"bigg_id\": \"fru_e\", \"label_y\": 1405.8675537109375}, \"1576827\": {\"node_is_primary\": true, \"name\": \"Nicotinamide adenine dinucleotide - reduced\", \"label_x\": 4322.0859375, \"node_type\": \"metabolite\", \"y\": 2478.91357421875, \"x\": 4417.8271484375, \"bigg_id\": \"nadh_c\", \"label_y\": 2462.79833984375}, \"1576824\": {\"node_is_primary\": false, \"name\": \"Nicotinamide adenine dinucleotide - reduced\", \"label_x\": 2347.0, \"node_type\": \"metabolite\", \"y\": 3935.0, \"x\": 2333.0, \"bigg_id\": \"nadh_c\", \"label_y\": 3921.0}, \"1576825\": {\"node_is_primary\": false, \"name\": \"Coenzyme A\", \"label_x\": 2140.0, \"node_type\": \"metabolite\", \"y\": 3860.0, \"x\": 2130.0, \"bigg_id\": \"coa_c\", \"label_y\": 3840.0}, \"1576822\": {\"node_is_primary\": false, \"name\": \"Nicotinamide adenine dinucleotide\", \"label_x\": 2496.0, \"node_type\": \"metabolite\", \"y\": 4065.0, \"x\": 2482.0, \"bigg_id\": \"nad_c\", \"label_y\": 4051.0}, \"1576988\": {\"y\": 3868.0, \"x\": 4116.0, \"node_type\": \"midmarker\"}, \"1576820\": {\"node_is_primary\": false, \"name\": \"H+\", \"label_x\": 4273.546875, \"node_type\": \"metabolite\", \"y\": 2570.0, \"x\": 4310.0, \"bigg_id\": \"h_c\", \"label_y\": 2604.6689453125}, \"1576821\": {\"node_is_primary\": false, \"name\": \"Pyruvate\", \"label_x\": 692.5455932617188, \"node_type\": \"metabolite\", \"y\": 1237.785400390625, \"x\": 714.76025390625, \"bigg_id\": \"pyr_c\", \"label_y\": 1207.785400390625}, \"1576985\": {\"y\": 4918.0, \"x\": 3991.0, \"node_type\": \"midmarker\"}, \"1576987\": {\"y\": 3848.0, \"x\": 4116.0, \"node_type\": \"multimarker\"}, \"1576986\": {\"y\": 3888.0, \"x\": 4116.0, \"node_type\": \"multimarker\"}, \"1576981\": {\"y\": 4305.0, \"x\": 1715.0, \"node_type\": \"multimarker\"}, \"1576980\": {\"y\": 4937.93359375, \"x\": 2500.0, \"node_type\": \"midmarker\"}, \"1576828\": {\"node_is_primary\": false, \"name\": \"Nicotinamide adenine dinucleotide\", \"label_x\": 2070.0, \"node_type\": \"metabolite\", \"y\": 3860.0, \"x\": 2070.0, \"bigg_id\": \"nad_c\", \"label_y\": 3840.0}, \"1576829\": {\"node_is_primary\": true, \"name\": \"Ethanol\", \"label_x\": 2522.0, \"node_type\": \"metabolite\", \"y\": 4228.0, \"x\": 2501.0, \"bigg_id\": \"etoh_c\", \"label_y\": 4206.0}, \"1576570\": {\"node_is_primary\": true, \"name\": \"Succinyl-CoA\", \"label_x\": 3772.230712890625, \"node_type\": \"metabolite\", \"y\": 3193.0, \"x\": 3739.0, \"bigg_id\": \"succoa_c\", \"label_y\": 3193.479248046875}, \"1576571\": {\"node_is_primary\": false, \"name\": \"ADP\", \"label_x\": 3788.0, \"node_type\": \"metabolite\", \"y\": 3116.0, \"x\": 3768.0, \"bigg_id\": \"adp_c\", \"label_y\": 3126.0}, \"1576572\": {\"node_is_primary\": false, \"name\": \"ATP\", \"label_x\": 1165.0, \"node_type\": \"metabolite\", \"y\": 1645.0, \"x\": 1145.0, \"bigg_id\": \"atp_c\", \"label_y\": 1645.0}, \"1576573\": {\"node_is_primary\": false, \"name\": \"ADP\", \"label_x\": 1165.0, \"node_type\": \"metabolite\", \"y\": 1805.0, \"x\": 1145.0, \"bigg_id\": \"adp_c\", \"label_y\": 1805.0}, \"1576574\": {\"node_is_primary\": false, \"name\": \"H+\", \"label_x\": 1165.0, \"node_type\": \"metabolite\", \"y\": 1855.0, \"x\": 1145.0, \"bigg_id\": \"h_c\", \"label_y\": 1855.0}, \"1576575\": {\"node_is_primary\": true, \"name\": \"Glyceraldehyde 3-phosphate\", \"label_x\": 1085.0, \"node_type\": \"metabolite\", \"y\": 2195.0, \"x\": 1055.0, \"bigg_id\": \"g3p_c\", \"label_y\": 2195.0}, \"1576576\": {\"node_is_primary\": false, \"name\": \"ATP\", \"label_x\": 4474.0, \"node_type\": \"metabolite\", \"y\": 3755.0, \"x\": 4440.0, \"bigg_id\": \"atp_c\", \"label_y\": 3753.0}, \"1576577\": {\"node_is_primary\": false, \"name\": \"Ammonium\", \"label_x\": 4477.0, \"node_type\": \"metabolite\", \"y\": 3805.0, \"x\": 4440.0, \"bigg_id\": \"nh4_c\", \"label_y\": 3811.0}, \"1576578\": {\"node_is_primary\": true, \"name\": \"L-Glutamine\", \"label_x\": 4245.0, \"node_type\": \"metabolite\", \"y\": 4094.0, \"x\": 4246.0, \"bigg_id\": \"gln__L_c\", \"label_y\": 4130.0}, \"1576579\": {\"node_is_primary\": false, \"name\": \"ADP\", \"label_x\": 4454.0, \"node_type\": \"metabolite\", \"y\": 3954.0, \"x\": 4434.0, \"bigg_id\": \"adp_c\", \"label_y\": 3954.0}, \"1576928\": {\"y\": 1900.0, \"x\": 4660.0, \"node_type\": \"midmarker\"}, \"1576651\": {\"node_is_primary\": false, \"name\": \"ATP\", \"label_x\": 1835.0, \"node_type\": \"metabolite\", \"y\": 4335.0, \"x\": 1815.0, \"bigg_id\": \"atp_c\", \"label_y\": 4335.0}, \"1576650\": {\"node_is_primary\": false, \"name\": \"H+\", \"label_x\": 2620.0, \"node_type\": \"metabolite\", \"y\": 4400.0, \"x\": 2600.0, \"bigg_id\": \"h_c\", \"label_y\": 4400.0}, \"1576653\": {\"node_is_primary\": false, \"name\": \"ADP\", \"label_x\": 1835.0, \"node_type\": \"metabolite\", \"y\": 4225.0, \"x\": 1815.0, \"bigg_id\": \"adp_c\", \"label_y\": 4225.0}, \"1576652\": {\"node_is_primary\": true, \"name\": \"Acetate\", \"label_x\": 1745.0, \"node_type\": \"metabolite\", \"y\": 4385.0, \"x\": 1715.0, \"bigg_id\": \"ac_c\", \"label_y\": 4385.0}, \"1576655\": {\"node_is_primary\": false, \"name\": \"H+\", \"label_x\": 4002.0, \"node_type\": \"metabolite\", \"y\": 3982.0, \"x\": 4029.0, \"bigg_id\": \"h_c\", \"label_y\": 3986.0}, \"1576654\": {\"node_is_primary\": false, \"name\": \"Nicotinamide adenine dinucleotide phosphate - reduced\", \"label_x\": 3993.9423828125, \"node_type\": \"metabolite\", \"y\": 3932.0, \"x\": 4029.0, \"bigg_id\": \"nadph_c\", \"label_y\": 3909.05029296875}, \"1576657\": {\"node_is_primary\": true, \"name\": \"Fumarate\", \"label_x\": 2879.817626953125, \"node_type\": \"metabolite\", \"y\": 650.0, \"x\": 2840.0, \"bigg_id\": \"fum_e\", \"label_y\": 635.0911865234375}, \"1576656\": {\"node_is_primary\": false, \"name\": \"Nicotinamide adenine dinucleotide phosphate\", \"label_x\": 3957.985595703125, \"node_type\": \"metabolite\", \"y\": 3759.0, \"x\": 4043.0, \"bigg_id\": \"nadp_c\", \"label_y\": 3736.215576171875}, \"1576659\": {\"node_is_primary\": false, \"name\": \"H+\", \"label_x\": 2954.28076171875, \"node_type\": \"metabolite\", \"y\": 1010.0, \"x\": 2940.0, \"bigg_id\": \"h_c\", \"label_y\": 1038.4132080078125}, \"1576658\": {\"node_is_primary\": false, \"name\": \"H+\", \"label_x\": 2962.214599609375, \"node_type\": \"metabolite\", \"y\": 700.0, \"x\": 2940.0, \"bigg_id\": \"h_e\", \"label_y\": 718.4132080078125}, \"1576912\": {\"y\": 3995.0, \"x\": 1255.0, \"node_type\": \"multimarker\"}, \"1576913\": {\"y\": 3995.0, \"x\": 1415.0, \"node_type\": \"multimarker\"}, \"1576910\": {\"y\": 3330.0, \"x\": 3776.0, \"node_type\": \"multimarker\"}, \"1576911\": {\"y\": 3995.0, \"x\": 1335.0, \"node_type\": \"midmarker\"}, \"1576916\": {\"y\": 3526.0, \"x\": 1404.0, \"node_type\": \"midmarker\"}, \"1576917\": {\"y\": 3554.0, \"x\": 1485.0, \"node_type\": \"multimarker\"}, \"1576914\": {\"y\": 1660.0, \"x\": 4970.0, \"node_type\": \"midmarker\"}, \"1576918\": {\"y\": 3503.0, \"x\": 1335.0, \"node_type\": \"multimarker\"}, \"1576919\": {\"y\": 4520.0, \"x\": 2880.0, \"node_type\": \"multimarker\"}, \"1577028\": {\"y\": 3360.0, \"x\": 2310.0, \"node_type\": \"multimarker\"}, \"1577021\": {\"y\": 3918.0, \"x\": 2840.0, \"node_type\": \"multimarker\"}, \"1577020\": {\"y\": 3837.0, \"x\": 2744.0, \"node_type\": \"multimarker\"}, \"1577023\": {\"y\": 3945.0, \"x\": 375.0, \"node_type\": \"multimarker\"}, \"1577022\": {\"y\": 3886.0, \"x\": 2798.0, \"node_type\": \"midmarker\"}, \"1577025\": {\"y\": 3945.0, \"x\": 505.0, \"node_type\": \"midmarker\"}, \"1577024\": {\"y\": 3945.0, \"x\": 645.0, \"node_type\": \"multimarker\"}, \"1577027\": {\"y\": 3410.0, \"x\": 2220.0, \"node_type\": \"multimarker\"}, \"1577026\": {\"y\": 3390.0, \"x\": 2260.0, \"node_type\": \"midmarker\"}, \"1576967\": {\"y\": 4604.0, \"x\": 3621.0, \"node_type\": \"midmarker\"}, \"1576966\": {\"y\": 1735.0, \"x\": 2175.0, \"node_type\": \"multimarker\"}, \"1576965\": {\"y\": 1695.0, \"x\": 2175.0, \"node_type\": \"midmarker\"}, \"1576964\": {\"y\": 1645.0, \"x\": 2175.0, \"node_type\": \"multimarker\"}, \"1576962\": {\"y\": 4924.0, \"x\": 3257.0, \"node_type\": \"midmarker\"}, \"1576961\": {\"y\": 4614.0, \"x\": 3257.0, \"node_type\": \"midmarker\"}, \"1576960\": {\"y\": 3341.0, \"x\": 2582.0, \"node_type\": \"multimarker\"}, \"1576969\": {\"y\": 3228.0, \"x\": 5032.0, \"node_type\": \"midmarker\"}, \"1576499\": {\"node_is_primary\": false, \"name\": \"H+\", \"label_x\": 3522.892333984375, \"node_type\": \"metabolite\", \"y\": 1080.287841796875, \"x\": 3510.0, \"bigg_id\": \"h_c\", \"label_y\": 1099.0072021484375}, \"1576999\": {\"y\": 1041.710205078125, \"x\": 1056.5867919921875, \"node_type\": \"multimarker\"}}, \"canvas\": {\"y\": 314.36893920898433, \"x\": 7.857062530517567, \"height\": 4860.457037353515, \"width\": 5894.515691375733}, \"text_labels\": {}}]'" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "builder._loaded_map_json" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.5" + }, + "widgets": { + "application/vnd.jupyter.widget-state+json": { + "state": {}, + "version_major": 2, + "version_minor": 0 + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/python_api.rst b/docs/python_api.rst index 113df56c..b11a65b2 100644 --- a/docs/python_api.rst +++ b/docs/python_api.rst @@ -8,16 +8,8 @@ Python API .. _`cache`: -Cache ------ - -.. autofunction:: get_cache_dir - -.. autofunction:: clear_cache - -.. autofunction:: list_cached_maps - -.. autofunction:: list_cached_models +Map Server +---------- .. autofunction:: list_available_maps diff --git a/jupyter/lab-extension.js b/jupyter/lab-extension.js new file mode 100644 index 00000000..6fcdc28d --- /dev/null +++ b/jupyter/lab-extension.js @@ -0,0 +1,13 @@ +const escher = require('../dist/escher.min') +const base = require('@jupyter-widgets/base') + +module.exports = { + id: 'jupyter.extensions.jupyter-escher', + requires: [base.IJupyterWidgetRegistry], + activate: (app, widgets) => widgets.registerWidget({ + name: 'jupyter-escher', + version: escher.version, + exports: escher.initializeJupyterWidget() + }), + autoStart: true +} diff --git a/jupyter/notebook-extension.js b/jupyter/notebook-extension.js new file mode 100644 index 00000000..56cb2c58 --- /dev/null +++ b/jupyter/notebook-extension.js @@ -0,0 +1,34 @@ +// This file contains the javascript that is run when the notebook is loaded. +// It contains some requirejs configuration and the `load_ipython_extension` +// which is required for any notebook extension. +// +// Some static assets may be required by the custom widget javascript. The base +// url for the notebook is not known at build time and is therefore computed +// dynamically. +window.__webpack_public_path__ = document.querySelector('body').getAttribute('data-base-url') + 'nbextensions/jupyter-escher' + +// Pull jupyter object out when importing escher +window.define( + 'jupyter-escher-intercept', + ['escher'], + escher => escher.initializeJupyterWidget() +) + +// Configure requirejs +if (window.require) { + window.require.config({ + map: { + '*': { + 'jupyter-escher': 'jupyter-escher-intercept' + }, + 'jupyter-escher-intercept': { + escher: 'nbextensions/jupyter-escher/escher.min' + } + } + }) +} + +// Export the required load_ipython_extension +module.exports = { + load_ipython_extension: () => {} +} diff --git a/package.json b/package.json index 392c0939..86656961 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ "repository": "https://github.com/zakandrewking/escher", "bugs": "https://github.com/zakandrewking/escher/issues", "files": [ - "dist/*" + "dist/*", + "jupyter/*" ], "main": "/dist/escher.js", "engines": { @@ -53,6 +54,7 @@ "webpack-node-externals": "^1.6.0" }, "dependencies": { + "@jupyter-widgets/base": "^1.1.10", "baconjs": "^0.7.71", "d3-brush": "https://github.com/zakandrewking/d3-brush.git", "d3-drag": "^1.0.2", @@ -72,9 +74,9 @@ "build": "./node_modules/.bin/webpack --config webpack.prod.js", "watch": "./node_modules/.bin/webpack --config webpack.prod.js --watch", "start": "./node_modules/.bin/webpack-dev-server --config webpack.dev.js", - "clean": "rm -r dist/*", + "clean": "rm -r dist/* && rm py/escher/static/*", "test": "./node_modules/.bin/mocha-webpack --webpack-config webpack.test.js \"src/tests/*.js\"", - "copy": "cp package.json py/escher/ && cp dist/* py/escher/static/escher/ && cp jsonschema/* py/escher/static/jsonschema/", + "copy": "cp package.json py/escher/static/ && cp jupyter/notebook-extension.js py/escher/static/extension.js && cp dist/escher.min.* py/escher/static/ && cp jsonschema/* py/escher/static/", "coverage": "./node_modules/.bin/cross-env NODE_ENV=coverage ./node_modules/.bin/nyc --reporter=text-lcov npm run test | ./node_modules/.bin/coveralls" }, "nyc": { @@ -83,5 +85,8 @@ ], "instrument": false, "sourceMap": false + }, + "jupyterlab": { + "extension": "jupyter/lab-extension" } } diff --git a/py/MANIFEST.in b/py/MANIFEST.in new file mode 100644 index 00000000..aac14555 --- /dev/null +++ b/py/MANIFEST.in @@ -0,0 +1,2 @@ +recursive-include escher/static *.* +include jupyter-escher.json \ No newline at end of file diff --git a/py/README.md b/py/README.md index c1ba6b0d..31ce8b46 100644 --- a/py/README.md +++ b/py/README.md @@ -1,10 +1,3 @@ -Escher Python Package ---------------------- +For documentation on the Escher Python package, see: -To install from source, run: - -``` -python setup.py install -# or -python setup.py develop -``` +https://escher.readthedocs.io/en/stable/escher-python.html diff --git a/py/escher/__init__.py b/py/escher/__init__.py index 265f18fc..499509ae 100644 --- a/py/escher/__init__.py +++ b/py/escher/__init__.py @@ -1,6 +1,20 @@ -from escher.version import __version__, __schema_version__, __map_model_version__ +from escher.version import ( + __version__, + __schema_version__, + __map_model_version__, +) -from escher.plots import (Builder, get_cache_dir, clear_cache, list_cached_maps, - list_cached_models, list_available_maps, - list_available_models) +from escher.plots import ( + Builder, + list_available_maps, + list_available_models, +) + +def _jupyter_nbextension_paths(): + return [{ + 'section': 'notebook', + 'src': 'static', + 'dest': 'jupyter-escher', + 'require': 'jupyter-escher/extension' + }] diff --git a/py/escher/appdirs.py b/py/escher/appdirs.py deleted file mode 100644 index 94c592fc..00000000 --- a/py/escher/appdirs.py +++ /dev/null @@ -1,346 +0,0 @@ -#!/usr/bin/env python -# Copyright (c) 2005-2010 ActiveState Software Inc. - -"""Utilities for determining application-specific dirs. - -See for details and usage. -""" -# Dev Notes: -# - MSDN on where to store app data files: -# http://support.microsoft.com/default.aspx?scid=kb;en-us;310294#XSLTH3194121123120121120120 -# - Mac OS X: http://developer.apple.com/documentation/MacOSX/Conceptual/BPFileSystem/index.html -# - XDG spec for Un*x: http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html - -__version_info__ = (1, 2, 0) -__version__ = '.'.join(map(str, __version_info__)) - - -import sys -import os - -PY3 = sys.version_info[0] == 3 - -if PY3: - unicode = str - -class AppDirsError(Exception): - pass - - - -def user_data_dir(appname, appauthor=None, version=None, roaming=False): - r"""Return full path to the user-specific data dir for this application. - - "appname" is the name of application. - "appauthor" (only required and used on Windows) is the name of the - appauthor or distributing body for this application. Typically - it is the owning company name. - "version" is an optional version path element to append to the - path. You might want to use this if you want multiple versions - of your app to be able to run independently. If used, this - would typically be ".". - "roaming" (boolean, default False) can be set True to use the Windows - roaming appdata directory. That means that for users on a Windows - network setup for roaming profiles, this user data will be - sync'd on login. See - - for a discussion of issues. - - Typical user data directories are: - Mac OS X: ~/Library/Application Support/ - Unix: ~/.config/ # or in $XDG_CONFIG_HOME if defined - Win XP (not roaming): C:\Documents and Settings\\Application Data\\ - Win XP (roaming): C:\Documents and Settings\\Local Settings\Application Data\\ - Win 7 (not roaming): C:\Users\\AppData\Local\\ - Win 7 (roaming): C:\Users\\AppData\Roaming\\ - - For Unix, we follow the XDG spec and support $XDG_CONFIG_HOME. We don't - use $XDG_DATA_HOME as that data dir is mostly used at the time of - installation, instead of the application adding data during runtime. - Also, in practice, Linux apps tend to store their data in - "~/.config/" instead of "~/.local/share/". - """ - if sys.platform.startswith("win"): - if appauthor is None: - raise AppDirsError("must specify 'appauthor' on Windows") - const = roaming and "CSIDL_APPDATA" or "CSIDL_LOCAL_APPDATA" - path = os.path.join(_get_win_folder(const), appauthor, appname) - elif sys.platform == 'darwin': - path = os.path.join( - os.path.expanduser('~/Library/Application Support/'), - appname) - else: - path = os.path.join( - os.getenv('XDG_CONFIG_HOME', os.path.expanduser("~/.config")), - appname.lower()) - if version: - path = os.path.join(path, version) - return path - - -def site_data_dir(appname, appauthor=None, version=None): - """Return full path to the user-shared data dir for this application. - - "appname" is the name of application. - "appauthor" (only required and used on Windows) is the name of the - appauthor or distributing body for this application. Typically - it is the owning company name. - "version" is an optional version path element to append to the - path. You might want to use this if you want multiple versions - of your app to be able to run independently. If used, this - would typically be ".". - - Typical user data directories are: - Mac OS X: /Library/Application Support/ - Unix: /etc/xdg/ - Win XP: C:\Documents and Settings\All Users\Application Data\\ - Vista: (Fail! "C:\ProgramData" is a hidden *system* directory on Vista.) - Win 7: C:\ProgramData\\ # Hidden, but writeable on Win 7. - - For Unix, this is using the $XDG_CONFIG_DIRS[0] default. - - WARNING: Do not use this on Windows. See the Vista-Fail note above for why. - """ - if sys.platform.startswith("win"): - if appauthor is None: - raise AppDirsError("must specify 'appauthor' on Windows") - path = os.path.join(_get_win_folder("CSIDL_COMMON_APPDATA"), - appauthor, appname) - elif sys.platform == 'darwin': - path = os.path.join( - os.path.expanduser('/Library/Application Support'), - appname) - else: - # XDG default for $XDG_CONFIG_DIRS[0]. Perhaps should actually - # *use* that envvar, if defined. - path = "/etc/xdg/"+appname.lower() - if version: - path = os.path.join(path, version) - return path - - -def user_cache_dir(appname, appauthor=None, version=None, opinion=True): - r"""Return full path to the user-specific cache dir for this application. - - "appname" is the name of application. - "appauthor" (only required and used on Windows) is the name of the - appauthor or distributing body for this application. Typically - it is the owning company name. - "version" is an optional version path element to append to the - path. You might want to use this if you want multiple versions - of your app to be able to run independently. If used, this - would typically be ".". - "opinion" (boolean) can be False to disable the appending of - "Cache" to the base app data dir for Windows. See - discussion below. - - Typical user cache directories are: - Mac OS X: ~/Library/Caches/ - Unix: ~/.cache/ (XDG default) - Win XP: C:\Documents and Settings\\Local Settings\Application Data\\\Cache - Vista: C:\Users\\AppData\Local\\\Cache - - On Windows the only suggestion in the MSDN docs is that local settings go in - the `CSIDL_LOCAL_APPDATA` directory. This is identical to the non-roaming - app data dir (the default returned by `user_data_dir` above). Apps typically - put cache data somewhere *under* the given dir here. Some examples: - ...\Mozilla\Firefox\Profiles\\Cache - ...\Acme\SuperApp\Cache\1.0 - OPINION: This function appends "Cache" to the `CSIDL_LOCAL_APPDATA` value. - This can be disabled with the `opinion=False` option. - """ - if sys.platform.startswith("win"): - if appauthor is None: - raise AppDirsError("must specify 'appauthor' on Windows") - path = os.path.join(_get_win_folder("CSIDL_LOCAL_APPDATA"), - appauthor, appname) - if opinion: - path = os.path.join(path, "Cache") - elif sys.platform == 'darwin': - path = os.path.join( - os.path.expanduser('~/Library/Caches'), - appname) - else: - path = os.path.join( - os.getenv('XDG_CACHE_HOME', os.path.expanduser('~/.cache')), - appname.lower()) - if version: - path = os.path.join(path, version) - return path - -def user_log_dir(appname, appauthor=None, version=None, opinion=True): - r"""Return full path to the user-specific log dir for this application. - - "appname" is the name of application. - "appauthor" (only required and used on Windows) is the name of the - appauthor or distributing body for this application. Typically - it is the owning company name. - "version" is an optional version path element to append to the - path. You might want to use this if you want multiple versions - of your app to be able to run independently. If used, this - would typically be ".". - "opinion" (boolean) can be False to disable the appending of - "Logs" to the base app data dir for Windows, and "log" to the - base cache dir for Unix. See discussion below. - - Typical user cache directories are: - Mac OS X: ~/Library/Logs/ - Unix: ~/.cache//log # or under $XDG_CACHE_HOME if defined - Win XP: C:\Documents and Settings\\Local Settings\Application Data\\\Logs - Vista: C:\Users\\AppData\Local\\\Logs - - On Windows the only suggestion in the MSDN docs is that local settings - go in the `CSIDL_LOCAL_APPDATA` directory. (Note: I'm interested in - examples of what some windows apps use for a logs dir.) - - OPINION: This function appends "Logs" to the `CSIDL_LOCAL_APPDATA` - value for Windows and appends "log" to the user cache dir for Unix. - This can be disabled with the `opinion=False` option. - """ - if sys.platform == "darwin": - path = os.path.join( - os.path.expanduser('~/Library/Logs'), - appname) - elif sys.platform == "win32": - path = user_data_dir(appname, appauthor, version); version=False - if opinion: - path = os.path.join(path, "Logs") - else: - path = user_cache_dir(appname, appauthor, version); version=False - if opinion: - path = os.path.join(path, "log") - if version: - path = os.path.join(path, version) - return path - - -class AppDirs(object): - """Convenience wrapper for getting application dirs.""" - def __init__(self, appname, appauthor, version=None, roaming=False): - self.appname = appname - self.appauthor = appauthor - self.version = version - self.roaming = roaming - @property - def user_data_dir(self): - return user_data_dir(self.appname, self.appauthor, - version=self.version, roaming=self.roaming) - @property - def site_data_dir(self): - return site_data_dir(self.appname, self.appauthor, - version=self.version) - @property - def user_cache_dir(self): - return user_cache_dir(self.appname, self.appauthor, - version=self.version) - @property - def user_log_dir(self): - return user_log_dir(self.appname, self.appauthor, - version=self.version) - - - - -#---- internal support stuff - -def _get_win_folder_from_registry(csidl_name): - """This is a fallback technique at best. I'm not sure if using the - registry for this guarantees us the correct answer for all CSIDL_* - names. - """ - import _winreg - - shell_folder_name = { - "CSIDL_APPDATA": "AppData", - "CSIDL_COMMON_APPDATA": "Common AppData", - "CSIDL_LOCAL_APPDATA": "Local AppData", - }[csidl_name] - - key = _winreg.OpenKey(_winreg.HKEY_CURRENT_USER, - r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders") - dir, type = _winreg.QueryValueEx(key, shell_folder_name) - return dir - -def _get_win_folder_with_pywin32(csidl_name): - from win32com.shell import shellcon, shell - dir = shell.SHGetFolderPath(0, getattr(shellcon, csidl_name), 0, 0) - # Try to make this a unicode path because SHGetFolderPath does - # not return unicode strings when there is unicode data in the - # path. - try: - dir = unicode(dir) - - # Downgrade to short path name if have highbit chars. See - # . - has_high_char = False - for c in dir: - if ord(c) > 255: - has_high_char = True - break - if has_high_char: - try: - import win32api - dir = win32api.GetShortPathName(dir) - except ImportError: - pass - except UnicodeError: - pass - return dir - -def _get_win_folder_with_ctypes(csidl_name): - import ctypes - - csidl_const = { - "CSIDL_APPDATA": 26, - "CSIDL_COMMON_APPDATA": 35, - "CSIDL_LOCAL_APPDATA": 28, - }[csidl_name] - - buf = ctypes.create_unicode_buffer(1024) - ctypes.windll.shell32.SHGetFolderPathW(None, csidl_const, None, 0, buf) - - # Downgrade to short path name if have highbit chars. See - # . - has_high_char = False - for c in buf: - if ord(c) > 255: - has_high_char = True - break - if has_high_char: - buf2 = ctypes.create_unicode_buffer(1024) - if ctypes.windll.kernel32.GetShortPathNameW(buf.value, buf2, 1024): - buf = buf2 - - return buf.value - -if sys.platform == "win32": - try: - import win32com.shell - _get_win_folder = _get_win_folder_with_pywin32 - except ImportError: - try: - import ctypes - _get_win_folder = _get_win_folder_with_ctypes - except ImportError: - _get_win_folder = _get_win_folder_from_registry - - - -#---- self test code - -if __name__ == "__main__": - appname = "MyApp" - appauthor = "MyCompany" - - props = ("user_data_dir", "site_data_dir", "user_cache_dir", - "user_log_dir") - - print("-- app dirs (without optional 'version')") - dirs = AppDirs(appname, appauthor, version="1.0") - for prop in props: - print("%s: %s" % (prop, getattr(dirs, prop))) - - print("\n-- app dirs (with optional 'version')") - dirs = AppDirs(appname, appauthor) - for prop in props: - print("%s: %s" % (prop, getattr(dirs, prop))) diff --git a/py/escher/convert_map.py b/py/escher/convert_map.py deleted file mode 100755 index dd676e80..00000000 --- a/py/escher/convert_map.py +++ /dev/null @@ -1,876 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -"""This script has two purposes: - -1. Apply the attributes from a cobra model to an existing Escher -map. For instance, update modified reversibilities. - -2. Convert maps made with Escher beta versions to valid jsonschema/1-0-0 -maps. - -""" - - -from __future__ import print_function, unicode_literals - - -usage_string = """ -Usage: - -./convert_map.py {map path} {model path} - -OR - -python -m escher.convert_map {map path} {model path} -""" - - -try: - import cobra.io - import jsonschema -except ImportError: - raise Exception(('The Python packages jsonschema and COBRApy (0.3.0b3 or later) ' - 'are required for converting maps.')) -import sys -import json -import random -import string -import hashlib -from os.path import basename, join -import logging -try: - from urllib.request import urlopen -except ImportError: - from urllib import urlopen - -from escher.validate import validate_map, genes_for_gene_reaction_rule - -# configure logger -logging.basicConfig(stream=sys.stdout, level=logging.INFO) - - -def main(): - """Main entrypoint for convert_map. Instructions are at the top of this file.""" - # get the arguments - try: - in_file = sys.argv[1] - model_path = sys.argv[2] - except IndexError: - print(usage_string) - sys.exit() - - # load the cobra model - try: - model = cobra.io.load_json_model(model_path) - except (IOError, ValueError): - try: - model = cobra.io.read_sbml_model(model_path) - except IOError: - raise Exception('Could not load the model: %s' % model_path) - - # get the current map - with open(in_file, 'r') as f: - out = json.load(f) - - # convert the map - the_map = convert(out, model) - - # don't replace the file - out_file = in_file.replace('.json', '_converted.json') - logging.info('Saving validated map to %s' % out_file) - with open(out_file, 'w') as f: - json.dump(the_map, f, allow_nan=False) - -# ------------------------------------------------------------------------------ -# Functions for manipulating Escher maps as Python objects -# ------------------------------------------------------------------------------ - -class MissingDefaultAttribute(Exception): - pass - -def make_map(header, body): - return [header, body] - -def get_header(a_map): - return a_map[0] - -def set_header(a_map, value): - a_map[0] = value - -def get_body(a_map): - return a_map[1] - -def set_body(a_map, value): - a_map[1] = value - -def get_nodes(body): - return body['nodes'] - -def set_nodes(body, value): - body['nodes'] = value - -def get_reactions(body): - return body['reactions'] - -def set_reactions(body, value): - body['reactions'] = value - -def is_valid_header(val): - """Header must have these values.""" - return (isinstance(val, dict) and - all(x in val for x in - ['schema', 'homepage', 'map_name', 'map_id', 'map_description'])) - -def is_valid_body(val): - """Body must be a dictionary.""" - return isinstance(val, dict) - -def has_header_and_body(val): - """Check for header and body.""" - return (isinstance(val, list) and len(val) == 2 and - is_valid_header(get_header(val)) and is_valid_body(get_body(val))) - -# ------------------------------------------------------------------------------ -# Functions for fixing nested dictionaries & lists -# ------------------------------------------------------------------------------ - -def dict_with_required_elements(the_dict, required_attributes, get_default=None, - nullable=[], cast={}): - """Remove unsupported elements and provide defaults for missing - elements or elements with zero length (e.g. {}, []). - - Arguments - --------- - - the_dict: A dictionary. - - required_attributes: The attributes required in the dictionary. - - get_default: A function that takes the attribute name and the current - object, and returns a default value. If not function is provided, then - MissingDefaultAttribute is raised when an attribute is not present. - - nullable: A list of attributes that can be None. - - cast: A dictionary of attributes for keys and functions for values. - - """ - if type(the_dict) is not dict: - raise MissingDefaultAttribute('(Bad object)') - - def has_zero_length(o): - """Returns True if o has a length and it is zero.""" - try: - return len(o) == 0 - except TypeError: - return False - - def current_otherwise_default(name): - """Take the value in the current dict, or else provide a default.""" - if name not in the_dict or has_zero_length(the_dict[name]): - if get_default is not None: - default = get_default(name, the_dict) - the_dict[name] = default - else: - raise MissingDefaultAttribute(name) - elif the_dict[name] is None and name not in nullable: - raise MissingDefaultAttribute('%s (is None)' % name) - # casting - try: - new = cast[name](the_dict[name]) - except KeyError: - pass - else: - the_dict[name] = new - - # map over the dict - not_required = set(the_dict.keys()) - for name in required_attributes: - current_otherwise_default(name) - # remember the keys that are not required - try: - not_required.remove(name) - except KeyError: - pass - - # remove not required - for key in not_required: - del the_dict[key] - -def map_over_dict_with_deletions(the_dict, fix_function): - """Use this to map over dictionary. The fix function updates values, and returns - None to delete a value. - - Returns the dictionary. - - Arguments - --------- - - the_dict: A dictionary to fix. - - fix_function: A function that takes the dictionary key and value as - arguments, and that returns None if the key is deleted, otherwise - returns the updated value. - - """ - - # this will update the dictionary and return a set of deleted ids - deleted_keys = set() - for key, val in list(the_dict.items()): - updated_val = fix_function(key, val) - if updated_val is None: - # add to aggregation set - deleted_keys.add(key) - # delete the value - del the_dict[key] - else: - # set the new value - the_dict[key] = updated_val - - return the_dict - -def list_of_dicts_with_required_elements(a_list, required_attributes, - get_default=None, nullable=[], - cast={}): - """For a list (dictionary) of dictionaries with required attributes, check - each one. Returns the new list. - - Arguments - --------- - - a_list: A dictionary with values that are dictionaries. - - required_attributes: A list of required attributes for the internal - dictionaries. - - get_default: A function that takes the attribute name and the current - object, and returns a default value. If not function is provided, then - MissingDefaultAttribute is raised when an attribute is not present. - - """ - - def fix_a_value(val): - try: - dict_with_required_elements(val, required_attributes, - nullable=nullable, cast=cast) - return val - except MissingDefaultAttribute: - return None - - # fix each element in the list - return [y for y in (fix_a_value(x) for x in a_list) - if y is not None] - -def collection_of_dicts_with_required_elements(collection, required_attributes, - get_default=None, nullable=[], - cast={}): - """For a collection (dictionary) of dictionaries with required - attributes, check each one. Returns the new collection. - - Arguments - --------- - - collection: A dictionary with values that are dictionaries. - - required_attributes: A list of required attributes for the internal - dictionaries. - - get_default: A function that takes the attribute name and the current - object, and returns a default value. If not function is provided, then - MissingDefaultAttribute is raised when an attribute is not present. - - """ - - def fix_a_value(val): - try: - dict_with_required_elements(val, required_attributes, - nullable=nullable, cast=cast) - return val - except MissingDefaultAttribute: - return None - - # update the dictionary - for k, v in list(collection.items()): - new_value = fix_a_value(v) - if new_value is None: - del collection[k] - else: - collection[k] = new_value - - return collection - -# ------------------------------------------------------------------------------ -# Functions for cleaning up unconnected map elements -# ------------------------------------------------------------------------------ - -def remove_unconnected_nodes(nodes, reactions, node_ids_deleted=set()): - """Check for nodes with no connected segments in the reactions object, and - return a new nodes object with only connected nodes. - - Arguments - --------- - - nodes: A collection (dict) of nodes. - - reactions: A collection (dict) of reactions. - - node_ids_deleted: A set of previously deleted ids to update. - - """ - # update - def map_over_connected_node_ids(fn, reactions): - """Run fn with all node ids that have connected segments.""" - for reaction in reactions.values(): - for segment in reaction['segments'].values(): - fn(segment['from_node_id']) - fn(segment['to_node_id']) - - # use a set to keep track of connected nodes - nodes_with_segments = set() - add_node = nodes_with_segments.add - has_node = lambda x: x in nodes_with_segments - # update - map_over_connected_node_ids(add_node, reactions) - - # filter the nodes - def check_fn(key, value): - if has_node(key): - return value - else: - logging.debug('No segments for node %s. Deleting' % key) - return None - map_over_dict_with_deletions(nodes, check_fn) - -def remove_unconnected_segments(reactions, nodes): - """Check for segments with no connected nodes. - - Arguments - --------- - - reactions: A collection (dict) of reactions. - - nodes: A collection (dict) of nodes. - - """ - # use a set to keep track of connected nodes - node_ids = set(nodes.keys()) - is_node = lambda x: x in node_ids - - # filter the segments - def check_fn(key, value): - if is_node(value['from_node_id']) and is_node(value['to_node_id']): - return value - else: - logging.debug('Missing node for segment %s. Deleting' % key) - return None - for reaction in reactions.values(): - map_over_dict_with_deletions(reaction['segments'], check_fn) - -def remove_reactions_with_missing_metabolites(reactions, nodes): - """Check for reactions that do not have all of their metabolites. - - Arguments - --------- - - reactions: A collection (dict) of reactions. - - nodes: A collection (dict) of nodes. - - """ - # filter the reactions - def check_fn(reaction_id, reaction): - # get the metabolites - metabolite_ids = {x['bigg_id'] for x in reaction['metabolites']} - # look for matching segments - for segment_id, segment in reaction['segments'].items(): - # find node - for n in 'from_node_id', 'to_node_id': - node = nodes[segment[n]] - try: - metabolite_ids.remove(node['bigg_id']) - except KeyError: - pass - if len(metabolite_ids) > 0: - logging.info('Deleting reaction %s with missing metabolites %s' % (reaction['bigg_id'], - str(list(metabolite_ids)))) - return None - else: - return reaction - - map_over_dict_with_deletions(reactions, check_fn) - -# ------------------------------------------------------------------------------ -# Functions for converting maps -# ------------------------------------------------------------------------------ - -def old_map_to_new_schema(the_map, map_name=None, map_description=None): - """Convert any old map to match the latest schema. Returns a new map. - - Arguments - --------- - - the_map: An Escher map loaded as a Python object (e.g. json.load('my_map.json')). - - map_name: A name for the map. If a name is already present, this name - overrides it. - - map_description: A description for the map. If a name is already present, - this name overrides it. - - """ - - def add_default_header(body, name, description): - """Return a map with header and body.""" - - def check_for(var, name): - """Print a warning if var is None.""" - if var is None: - logging.warn('No {} for map'.format(name)) - return '' - return var - - new_id = hashlib.md5(json.dumps(body).encode('utf-8')).hexdigest() - default_header = { - "schema": "https://escher.github.io/escher/jsonschema/1-0-0#", - "homepage": "https://escher.github.io", - "map_name": check_for(name, 'name'), - "map_id": new_id, - "map_description": check_for(description, 'description') - } - logging.info('Map has the ID {}'.format(new_id)) - return make_map(default_header, body) - - def add_header_if_missing(a_map): - """Check for new, 2-level maps, and add the header.""" - - if has_header_and_body(a_map): - use_name = (get_header(a_map)['map_name'] if map_name is None else map_name) - use_description = (get_header(a_map)['map_description'] - if map_description is None else map_description) - return add_default_header(get_body(a_map), use_name, use_description) - elif is_valid_body(a_map): - return add_default_header(a_map, map_name, map_description) - else: - raise Exception('The map provided cannot be converted. It is not a valid Escher map.') - - def fix_body(body): - """Fill in necessary attributes for the body.""" - - def get_default(key, _): - """Get default value for key in body.""" - if key == 'canvas': - # default canvas - return {'x': -1440, 'y': -775, 'width': 4320, 'height': 2325} - else: - # other defaults - return {} - - def fix_canvas(canvas): - """Return a canvas with correct attributes.""" - canvas_keys = ['x', 'y', 'width', 'height'] - cast = {'x': float, 'y': float, 'width': float, 'height': float} - dict_with_required_elements(canvas, canvas_keys, cast=cast) - - def fix_text_labels(text_labels): - """Return a list of text_labels with correct attributes.""" - text_label_keys = ['x', 'y', 'text'] - cast = {'x': float, 'y': float} - collection_of_dicts_with_required_elements(text_labels, - text_label_keys, - cast=cast) - - # fix canvas before body so that a default canvas can be added if the - # canvas is invalid - try: - fix_canvas(body['canvas']) - except KeyError: - pass - except MissingDefaultAttribute: - del body['canvas'] - - # fix body - core_keys = ['nodes', 'reactions', 'text_labels', 'canvas'] - dict_with_required_elements(body, core_keys, get_default) - - # fix text labels - fix_text_labels(body['text_labels']) - - def fix_nodes(nodes): - """Fill in necessary attributes for the nodes. Returns nodes.""" - - def get_default_node_attr(name, obj): - """Return default values when possible. Otherwise, raise MissingDefaultAttribute.""" - if name == 'node_is_primary': - return False - elif name == 'name': - return '' - elif name == 'label_x' and 'x' in obj: - return obj['x'] - elif name == 'label_y' and 'y' in obj: - return obj['y'] - else: - raise MissingDefaultAttribute(name) - - def fix_a_node(node_id, node): - """Return an updated node, or None if the node is invalid.""" - - if not 'node_type' in node: - logging.debug('Deleting node %s with no node_type' % node_id) - return None - - elif node['node_type'] == 'metabolite': - met_keys = ['node_type', 'x', 'y', 'bigg_id', 'name', 'label_x', - 'label_y', 'node_is_primary'] - cast = {'x': float, 'y': float, 'label_x': float, 'label_y': float} - try: - dict_with_required_elements(node, met_keys, get_default_node_attr, cast=cast) - return node - except MissingDefaultAttribute as e: - logging.debug('Deleting node %s with missing attribute %s' % (node_id, e)) - return None - - elif node['node_type'] in ['multimarker', 'midmarker']: - marker_keys = ['node_type', 'x', 'y'] - cast = {'x': float, 'y': float} - try: - dict_with_required_elements(node, marker_keys, get_default_node_attr, cast=cast) - return node - except MissingDefaultAttribute as e: - logging.debug('Deleting node %s with missing attribute %s' % (node_id, e)) - return None - - else: - logging.debug('Deleting node %s with bad node_type %s' % (node_id, node['node_type'])) - return None - - # run fix functions - map_over_dict_with_deletions(nodes, fix_a_node) - - def fix_reactions(reactions): - """Fill in necessary attributes for the reactions. - - Returns reactions. - - """ - - def get_default_reaction_attr(name, obj): - """Return default values when possible. Otherwise, raise MissingDefaultAttribute.""" - if name in ['name', 'gene_reaction_rule']: - return '' - elif name == 'reversibility': - return True - elif name in ['genes', 'metabolites']: - return [] - else: - raise MissingDefaultAttribute(name) - - def fix_a_reaction(reaction_id, reaction): - """Return an updated reaction, or None if the reaction is invalid.""" - - def fix_segments(segments): - """Fix dictionary of segments with correct attributes.""" - def fix_a_segment(segment_id, segment): - segment_keys = ['from_node_id', 'to_node_id', 'b1', 'b2'] - def get_default_segment_attr(key, _): - if key in ['b1', 'b2']: - return None - else: - raise MissingDefaultAttribute(key) - try: - dict_with_required_elements(segment, segment_keys, - get_default_segment_attr, - nullable=['b1', 'b2']) - except MissingDefaultAttribute as e: - logging.debug('Deleting segment %s with missing attribute %s' % (segment_id, e)) - return None - - # check the beziers too - required_bezier_keys = ['x', 'y'] - cast = {'x': float, 'y': float} - for b in ['b1', 'b2']: - try: - dict_with_required_elements(segment[b], - required_bezier_keys, - cast=cast) - except MissingDefaultAttribute as e: - logging.debug('Deleting bezier %s with missing attribute %s in segment %s' % (b, e, segment_id)) - segment[b] = None - - return segment - - map_over_dict_with_deletions(segments, fix_a_segment) - - def fix_metabolites(metabolites): - """Return a list of metabolites with correct attributes.""" - metabolite_keys = ['coefficient', 'bigg_id'] - cast = {'coefficient': float} - return list_of_dicts_with_required_elements(metabolites, - metabolite_keys, - cast=cast) - - def fix_genes(genes): - """Return a list of genes with correct attributes.""" - gene_keys = ['bigg_id', 'name'] - def get_default_gene_attr(name, _): - if name == 'name': - return '' - else: - raise MissingDefaultAttribute(name) - return list_of_dicts_with_required_elements(genes, gene_keys, - get_default_gene_attr) - - # fix all the attributes - reaction_keys = ['name', 'bigg_id','reversibility', 'label_x', - 'label_y', 'gene_reaction_rule', 'genes', - 'metabolites', 'segments'] - cast = {'label_x': float, 'label_y': float} - try: - dict_with_required_elements(reaction, reaction_keys, - get_default_reaction_attr, - cast=cast) - except MissingDefaultAttribute as e: - logging.debug('Deleting reaction %s with missing attribute %s' % (reaction_id, e)) - return None - - # fix segments, metabolites, and genes - fix_segments(reaction['segments']) - # must have segments - if len(reaction['segments']) == 0: - logging.debug('Deleting reaction %s with no segments' % reaction_id) - return None - reaction['metabolites'] = fix_metabolites(reaction['metabolites']) - reaction['genes'] = fix_genes(reaction['genes']) - - return reaction - - # run the fix functions - map_over_dict_with_deletions(reactions, fix_a_reaction) - - - # make sure there is a body and a head - the_map = add_header_if_missing(the_map) - body = get_body(the_map) - - # add missing elements to body - fix_body(body) - - # fix the nodes - fix_nodes(get_nodes(body)) - - # fix the reactions - fix_reactions(get_reactions(body)) - - # delete any nodes with no segment - remove_unconnected_nodes(get_nodes(body), get_reactions(body)) - - # delete segments with no nodes - remove_unconnected_segments(get_reactions(body), get_nodes(body)) - - # delete reactions with missing metabolite segments - remove_reactions_with_missing_metabolites(get_reactions(body), get_nodes(body)) - - return the_map - - -def apply_id_mappings(the_map, reaction_id_mapping=None, - metabolite_id_mapping=None, gene_id_mapping=None): - """Convert bigg_ids in the map using the mappings dictionaries. - - Arguments - --------- - - the_map: The Escher map Python object. - - reaction_id_mapping: A dictionary with keys for existing bigg_ids and value for new bigg_ids. - - metabolite_id_mapping: A dictionary with keys for existing bigg_ids and value for new bigg_ids. - - gene_id_mapping: A dictionary with keys for existing bigg_ids and value for new bigg_ids. - - """ - - id_key = 'bigg_id' - - def check(a_dict, mapping): - """Try to change the value for id_key to a new value defined in mapping.""" - try: - new_id = mapping[a_dict[id_key]] - except KeyError: - pass - else: - a_dict[id_key] = new_id - return a_dict - - def apply_mapping_list(a_list, mapping): - """Use the mapping on each dict in the list. Returns a new list.""" - return [check(x, mapping) for x in a_list] - - def apply_mapping_dict(collection, mapping): - """Use the mapping on each dict in the collection (dict). Returns the dictionary.""" - for val in collection.values(): - check(val, mapping) - return collection - - body = get_body(the_map) - # reactions - if reaction_id_mapping is not None: - apply_mapping_dict(get_reactions(body), reaction_id_mapping) - # metabolites - if metabolite_id_mapping is not None: - apply_mapping_dict(get_nodes(body), metabolite_id_mapping) - # genes & metabolites in reactions - for reaction in get_reactions(body).values(): - if gene_id_mapping is not None: - reaction['genes'] = apply_mapping_list(reaction['genes'], gene_id_mapping) - if metabolite_id_mapping is not None: - reaction['metabolites'] = apply_mapping_list(reaction['metabolites'], metabolite_id_mapping) - - -def apply_cobra_model_to_map(the_map, model): - """Apply the COBRA model attributes (descriptive names, gene reaction rules, - reversibilities) to the map. - - Cleans up unconnected segments and nodes after deleting any nodes and - reactions not found in the cobra model. - - """ - - def apply_model_attributes(a_dict, dict_list, attribute_fns, collection_on='bigg_id'): - """For each attribute_fn, apply the values from the dict_list to given - dictionary, using the given IDs. Returns the dictionary, or None if a - matching object was not found in the DictList. - - """ - try: - on_id = a_dict[collection_on] - except KeyError: - return a_dict - try: - dl_object = dict_list.get_by_id(on_id) - except KeyError: - logging.info('Could not find %s in model. Deleting.' % on_id) - return None - else: - for attribute_fn in attribute_fns: - attribute_fn(a_dict, dl_object) - - return a_dict - - def apply_model_attributes_dict(collection, dict_list, attribute_fns, - collection_on='bigg_id'): - """For each attribute_fn, apply the values from the dict_list to the matching - results in the collection, using the given IDs. Returns the collection.""" - for key, val in list(collection.items()): - new_val = apply_model_attributes(val, dict_list, attribute_fns, - collection_on) - if new_val is None: - del collection[key] - - def apply_model_attributes_list(a_list, dict_list, attribute_fns, - collection_on='bigg_id'): - """For each attribute_fn, apply the values from the dict_list to the matching - results in the list of dicts, using the given IDs. Returns a new list.""" - return [y for y in (apply_model_attributes(x, dict_list, attribute_fns, collection_on) for x in a_list) - if y is not None] - - def get_attr_fn(attribute): - """Make a default attribute setting function.""" - def new_attr_fn(val, dl_object): - try: - val[attribute] = getattr(dl_object, attribute) - except AttributeError: - logging.debug('No %s found in DictList %s' % (attribute, on_id)) - return new_attr_fn - - def set_reversibility(reaction, cobra_reaction): - reaction['reversibility'] = (cobra_reaction.lower_bound < 0 and cobra_reaction.upper_bound > 0) - # reverse metabolites if reaction runs in reverse - rev_mult = (-1 if - (cobra_reaction.lower_bound < 0 and cobra_reaction.upper_bound <= 0) - else 1) - # use metabolites from reaction - reaction['metabolites'] = [{'bigg_id': met.id, 'coefficient': coeff * rev_mult} - for met, coeff in - cobra_reaction.metabolites.items()] - - def set_genes(reaction, cobra_reaction): - reaction['genes'] = [{'bigg_id': x.id, 'name': x.name} for x in cobra_reaction.genes] - - # vars - body = get_body(the_map) - - # compare reactions to model - reaction_attributes = [get_attr_fn('name'), get_attr_fn('gene_reaction_rule'), - set_reversibility, set_genes] - apply_model_attributes_dict(get_reactions(body), model.reactions, - reaction_attributes) - - # compare metabolites to model - metabolite_attributes = [get_attr_fn('name')] - apply_model_attributes_dict(get_nodes(body), model.metabolites, - metabolite_attributes) - - # delete any nodes with no segment - remove_unconnected_nodes(get_nodes(body), get_reactions(body)) - - # delete segments with no nodes - remove_unconnected_segments(get_reactions(body), get_nodes(body)) - - # delete reactions with missing metabolite segments - remove_reactions_with_missing_metabolites(get_reactions(body), get_nodes(body)) - - -def convert(the_map, model, map_name=None, map_description=None, - reaction_id_mapping=None, metabolite_id_mapping=None, - gene_id_mapping=None, debug=False): - """Convert an Escher map to the latest format using the COBRA model to update - content. Returns a new map. - - Arguments - --------- - - the_map: An Escher map loaded as a Python object (e.g. json.load('my_map.json')). - - model: A COBRA model. - - map_name: A name for the map. If a name is already present, this name - overrides it. - - map_description: A description for the map. If a name is already present, - this name overrides it. - - reaction_id_mapping: A dictionary with existing reaction IDs as keys and the - new reaction IDs as values. - - metabolite_id_mapping: A dictionary with existing metabolite IDs as keys and the - new metabolite IDs as values. - - gene_id_mapping: A dictionary with existing gene IDs as keys and the new - gene IDs as values. - - debug: Check the map against the schema at some intermediate steps. - - """ - - # make sure everything is up-to-date - new_map = old_map_to_new_schema(the_map, map_name=map_name, - map_description=map_description) - if debug: - validate_map(new_map) - - # apply the ids mappings - apply_id_mappings(new_map, reaction_id_mapping, metabolite_id_mapping, - gene_id_mapping) - if debug: - validate_map(new_map) - - # apply the new model - apply_cobra_model_to_map(new_map, model) - - validate_map(new_map) - return new_map - - -if __name__ == "__main__": - main() diff --git a/py/escher/generate_index.py b/py/escher/generate_index.py deleted file mode 100644 index de3a7a4f..00000000 --- a/py/escher/generate_index.py +++ /dev/null @@ -1,39 +0,0 @@ -from __future__ import print_function, unicode_literals - -import json -from os import listdir -from os.path import join, dirname, realpath, exists, isdir, relpath -import sys -from collections import defaultdict -from escher import __schema_version__, __map_model_version__ -from escher.urls import top_directory - -def generate_index(directory): - def list_cached(directory, kind): - l = [] - if isdir(directory): - for f in listdir(directory): - tf = join(directory, f) - if isdir(tf): - for f2 in listdir(tf): - if f2.endswith('.json'): - f2 = f2.replace('.json', '') - l.append({ 'organism': f, - kind + '_name': f2 }) - return l - return { 'maps': list_cached(join(directory, 'maps'), 'map'), - 'models': list_cached(join(directory, 'models'), 'model') } - -def main(): - directory = join(top_directory, __schema_version__, __map_model_version__) - if not exists(directory): - print('No directory to index') - sys.exit() - print('Indexing %s' % directory) - index = generate_index(directory) - with open(join(directory, 'index.json'), 'w') as f: - json.dump(index, f) - print('Saved index.json') - -if __name__=='__main__': - main() diff --git a/py/escher/plots.py b/py/escher/plots.py index b211da22..afbf3936 100644 --- a/py/escher/plots.py +++ b/py/escher/plots.py @@ -1,17 +1,16 @@ # -*- coding: utf-8 -*- - from __future__ import print_function, unicode_literals -from escher.quick_server import serve_and_open from escher.urls import get_url, root_directory -from escher.appdirs import user_cache_dir -from escher.generate_index import generate_index -from escher.version import __schema_version__, __map_model_version__ -from escher.util import query_yes_no, b64dump +from escher.util import b64dump +from escher.version import __version__ +import cobra +from cobra import Model +import ipywidgets as widgets +from traitlets import Unicode, Int, Instance, Dict, observe, validate import os -from os.path import (dirname, basename, abspath, join, isfile, isdir, exists, - expanduser) +from os.path import join, isfile, expanduser from warnings import warn try: from urllib.request import urlopen @@ -27,79 +26,16 @@ import string from tornado.escape import url_escape import shutil +from typing import Optional # set up jinja2 template location env = Environment(loader=PackageLoader('escher', 'templates')) -# cache management - -def get_cache_dir(versioned=True, name=None): - """Get the cache dir as a string, and make the directory if it does not already - exist. - - :param Boolean versioned: Whether to return the versioned path in the - cache. Escher maps for the latest version of - Escher are found in the versioned directory - (versioned = True), but maps for previous versions - of Escher can be found by visiting the parent - directory (versioned = False). - - :param string name: An optional subdirectory within the cache. If versioned - is False, then name is ignored. - - """ - cache_dir = user_cache_dir('escher', appauthor='Zachary King') - # add version - if versioned: - cache_dir = join(cache_dir, __schema_version__, __map_model_version__) - # add subdirectory - if name is not None: - cache_dir = join(cache_dir, name) - try: - os.makedirs(cache_dir) - except OSError: - pass - return cache_dir - -def clear_cache(different_cache_dir=None, ask=True): - """Empty the contents of the cache directory, including all versions of all maps - and models. - - :param string different_cache_dir: (Optional) The directory of another - cache. This is mainly for testing. - - :param Boolean ask: Whether to ask before deleting. - - """ - if ask and not query_yes_no('Are you sure you want to delete the contents of the cache?'): - return - - if different_cache_dir is None: - cache_dir = get_cache_dir(versioned=False) - else: - cache_dir = different_cache_dir - - for root, dirs, files in os.walk(cache_dir): - for f in files: - os.unlink(join(root, f)) - for d in dirs: - shutil.rmtree(join(root, d)) - -def local_index(cache_dir=get_cache_dir()): - return generate_index(cache_dir) - -def list_cached_maps(): - """Return a list of all cached maps.""" - return local_index()['maps'] - -def list_cached_models(): - """Return a list of all cached models.""" - return local_index()['models'] # server management def server_index(): - url = get_url('server_index', source='web', protocol='https') + url = get_url('server_index') try: download = urlopen(url) except URLError: @@ -108,76 +44,58 @@ def server_index(): index = json.loads(data) return index + def list_available_maps(): """Return a list of all maps available on the server""" return server_index()['maps'] + def list_available_models(): """Return a list of all models available on the server""" return server_index()['models'] + # download maps and models -def _json_for_name(name, kind, cache_dir): + +def _json_for_name(name: str, kind: str): # check the name name = name.replace('.json', '') def match_in_index(name, index, kind): - return [(obj['organism'], obj[kind + '_name']) for obj in index[kind + 's'] + return [(obj['organism'], obj[kind + '_name']) + for obj in index[kind + 's'] if obj[kind + '_name'] == name] - # first check the local index - match = match_in_index(name, local_index(cache_dir=cache_dir), kind) + try: + index = server_index() + except URLError: + raise Exception('Could not connect to the Escher server') + match = match_in_index(name, server_index(), kind) if len(match) == 0: - path = None - else: - org, name = match[0] - path = join(cache_dir, kind + 's', org, name + '.json') - - if path: - # load the file - with open(path, 'rb') as f: - return f.read().decode('utf-8') - # if the file is not present attempt to download - else: - try: - index = server_index() - except URLError: - raise Exception(('Could not find the %s %s in the cache, and could ' - 'not connect to the Escher server' % (kind, name))) - match = match_in_index(name, server_index(), kind) - if len(match) == 0: - raise Exception('Could not find the %s %s in the cache or on the server' % (kind, name)) - org, name = match[0] - url = (get_url(kind + '_download', source='web', protocol='https') + - '/'.join([url_escape(x, plus=False) for x in [org, name + '.json']])) - warn('%s not in cache. Attempting download from %s' % (kind.title(), url)) - try: - download = urlopen(url) - except URLError: - raise ValueError('No %s found in cache or at %s' % (kind, url)) - data = _decode_response(download) - # save the file - org_path = join(cache_dir, kind + 's', org) - try: - os.makedirs(org_path) - except OSError: - pass - with open(join(org_path, name + '.json'), 'w') as outfile: - outfile.write(data) - return data + raise Exception('Could not find the %s %s on the server' % (kind, name)) + org, name = match[0] + url = (get_url(kind + '_download') + + '/'.join([url_escape(x, plus=False) for x in [org, name + '.json']])) + print('Downloading %s from %s' % (kind.title(), url)) + try: + download = urlopen(url) + except URLError: + raise ValueError('No %s found in at %s' % (kind, url)) + data = _decode_response(download) + return data + -def model_json_for_name(model_name, cache_dir=get_cache_dir()): - return _json_for_name(model_name, 'model', cache_dir) +def model_json_for_name(model_name): + return _json_for_name(model_name, 'model') + + +def map_json_for_name(map_name): + return _json_for_name(map_name, 'map') -def map_json_for_name(map_name, cache_dir=get_cache_dir()): - return _json_for_name(map_name, 'map', cache_dir) # helper functions -def _get_an_id(): - return (''.join(random.choice(string.ascii_lowercase) - for _ in range(10))) def _decode_response(download): """Decode the urllib.response.addinfourl response.""" @@ -192,7 +110,8 @@ def _decode_response(download): data = data.decode('utf-8') return data -def _load_resource(resource, name, safe=False): + +def _load_resource(resource, name): """Load a resource that could be a file, URL, or json string.""" # if it's a url, download it if resource.startswith('http://') or resource.startswith('https://'): @@ -209,8 +128,6 @@ def _load_resource(resource, name, safe=False): # check for error with long filepath (or URL) on Windows is_file = False if is_file: - if (safe): - raise Exception('Cannot load resource from file with safe mode enabled.') try: with open(resource, 'rb') as f: loaded_resource = f.read().decode('utf-8') @@ -223,83 +140,80 @@ def _load_resource(resource, name, safe=False): try: _ = json.loads(resource) except ValueError as err: - raise ValueError('Could not load %s. Not valid json, url, or filepath' % name) + raise ValueError('Could not load %s. Not valid json, url, or filepath' + % name) else: return resource raise Exception('Could not load %s.' % name) -class Builder(object): - """A metabolic map that can be viewed, edited, and used to visualize data. +class Builder(widgets.DOMWidget): + """A Python wrapper for the Escher metabolic map. - This map will also show metabolic fluxes passed in during consruction. It - can be viewed as a standalone html inside a browswer. Alternately, the - respresentation inside an IPython notebook will also display the map. + This map will also show data on reactions, metabolites, or genes. - Maps are stored in json files and are stored in a cache directory. Maps - which are not found will be downloaded from a map repository if found. + The Builder is a Jupyter widget that can be viewed in a Jupyter notebook or + in Jupyter Lab. It can also be used to create a standalone HTML file for + the map with the save_html() function. - :param map_name: + Maps are downloaded from the Escher website if found by name. - A string specifying a map to be downloaded from the Escher web server, - or loaded from the cache. + :param int height: - :param map_json: + The height of the Escher Jupyter widget in pixels. - A JSON string, or a file path to a JSON file, or a URL specifying a JSON - file to be downloaded. + :param str map_name: - :param model: A Cobra model. + A string specifying a map to be downloaded from the Escher website. + + :param str map_json: + + A JSON string, or a file path to a JSON file, or a URL specifying a + JSON file to be downloaded. + + :param model: + + A COBRApy model. :param model_name: - A string specifying a model to be downloaded from the Escher web server, - or loaded from the cache. + A string specifying a model to be downloaded from the Escher web + server. :param model_json: - A JSON string, or a file path to a JSON file, or a URL specifying a JSON - file to be downloaded. + A JSON string, or a file path to a JSON file, or a URL specifying a + JSON file to be downloaded. :param embedded_css: - The CSS (as a string) to be embedded with the Escher SVG. + The CSS (as a string) to be embedded with the Escher SVG. In Jupyter, + if you change embedded_css on an existing builder instance, the Builder + must be restarted for this to take effect (e.g. by re-evaluating the + widget in a cell). You can use the default embedded css as a starting + point: + + https://github.com/zakandrewking/escher/blob/master/src/Builder-embed.css :param reaction_data: - A dictionary with keys that correspond to reaction ids and values that + A dictionary with keys that correspond to reaction IDs and values that will be mapped to reaction arrows and labels. :param metabolite_data: - A dictionary with keys that correspond to metabolite ids and values that - will be mapped to metabolite nodes and labels. + A dictionary with keys that correspond to metabolite IDs and values + that will be mapped to metabolite nodes and labels. :param gene_data: - A dictionary with keys that correspond to gene ids and values that will + A dictionary with keys that correspond to gene IDs and values that will be mapped to corresponding reactions. - :param local_host: - - A hostname that will be used for any local files. This is generally used - for using the notebook offline and for testing in the IPython Notebook - with modified Escher code. An example value for local_host is - 'http://localhost:7778/'. - - :param id: - - Specify an id to make the javascript data definitions unique. A random - id is chosen by default. - - :param safe: - - If True, then loading files from the filesytem is not allowed. This is - to ensure the safety of using Builder within a web server. - **Keyword Arguments** - These are defined in the Javascript API: + You can also pass in any of the following options as keyword arguments. The + details on each of these are provided in the JavaScript API documentation: - use_3d_transform - enable_search @@ -333,500 +247,269 @@ class Builder(object): - cofactors - enable_tooltips - All keyword arguments can also be set on an existing Builder object - using setter functions, e.g.: + All arguments can also be set by assigning the property of an an existing + Builder object, e.g.: .. code:: python - my_builder.set_reaction_styles(new_styles) + my_builder.map_name = 'iJO1366.Central metabolism' """ - def __init__(self, map_name=None, map_json=None, model=None, - model_name=None, model_json=None, embedded_css=None, - reaction_data=None, metabolite_data=None, gene_data=None, - local_host=None, id=None, safe=False, **kwargs): - - self.safe = safe - - # load the map - self.map_name = map_name - self.map_json = map_json - self.loaded_map_json = None - if map_name and map_json: - warn('map_json overrides map_name') - self._load_map() - # load the model - self.model = model - self.model_name = model_name - self.model_json = model_json - self.loaded_model_json = None - if sum([x is not None for x in (model, model_name, model_json)]) >= 2: - warn('model overrides model_json, and both override model_name') - self._load_model() - # set the args - self.reaction_data = reaction_data - self.metabolite_data = metabolite_data - self.gene_data = gene_data - self.local_host = local_host - - # remove illegal characters from css - try: - self.embedded_css = (embedded_css.replace('\n', '')) - except AttributeError: - self.embedded_css = None - # make the unique id - self.the_id = _get_an_id() if id is None else id - - # set up the options - self.options = [ - 'use_3d_transform', - 'enable_search', - 'fill_screen', - 'zoom_to_element', - 'full_screen_button', - 'starting_reaction', - 'unique_map_id', - 'primary_metabolite_radius', - 'secondary_metabolite_radius', - 'marker_radius', - 'gene_font_size', - 'hide_secondary_metabolites', - 'show_gene_reaction_rules', - 'hide_all_labels', - 'canvas_size_and_loc', - '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', - 'cofactors', - 'enable_tooltips', - ] - def get_getter_setter(o): - """Use a closure.""" - # create local fget and fset functions - fget = lambda self: getattr(self, '_%s' % o) - fset = lambda self, value: setattr(self, '_%s' % o, value) - return fget, fset - for option in self.options: - fget, fset = get_getter_setter(option) - # make the setter - setattr(self.__class__, 'set_%s' % option, fset) - # add property to self - setattr(self.__class__, option, property(fget)) - # add corresponding local variable - setattr(self, '_%s' % option, None) - - # set the kwargs - for key, val in kwargs.items(): - try: - getattr(self, 'set_%s' % key)(val) - except AttributeError: - print('Unrecognized keywork argument %s' % key) - - - def _load_model(self): - """Load the model. - - Try first from self.model, second from self.model_json, and - third from from self.model_name. - - """ - if self.model is not None: - try: - import cobra.io - except ImportError: - raise Exception(('The COBRApy package must be available to load ' - 'a COBRA model object')) - self.loaded_model_json = cobra.io.to_json(self.model) - elif self.model_json is not None: - self.loaded_model_json = _load_resource(self.model_json, - 'model_json', - safe=self.safe) - elif self.model_name is not None: - self.loaded_model_json = model_json_for_name(self.model_name) - - - def _load_map(self): - """Load the map from input map_json using _load_resource, or, secondarily, - from map_name. - - """ - if self.map_json is not None: - self.loaded_map_json = _load_resource(self.map_json, - 'map_json', - safe=self.safe) - elif self.map_name is not None: - self.loaded_map_json = map_json_for_name(self.map_name) - - - def _get_html(self, js_source='web', menu='none', scroll_behavior='pan', - html_wrapper=False, enable_editing=False, enable_keys=False, - minified_js=True, fill_screen=False, height='800px', - never_ask_before_quit=False, static_site_index_json=None, - protocol=None, ignore_bootstrap=False): - """Generate the Escher HTML. - - Arguments - -------- - - js_source: Can be one of the following: - 'web' - (Default) use js files from unpkg. - 'local' - Use compiled js files in the local escher installation. Works offline. - 'dev' - No longer necessary with source maps. This now gives the - same behavior as 'local'. - - menu: Menu bar options include: - 'none' - (Default) No menu or buttons. - 'zoom' - Just zoom buttons (does not require bootstrap). - 'all' - Menu and button bar (requires bootstrap). - - scroll_behavior: Scroll behavior options: - 'pan' - (Default) Pan the map. - 'zoom' - Zoom the map. - 'none' - No scroll events. - - minified_js: If True, use the minified version of JavaScript files. - - html_wrapper: If True, return a standalone html file. - - enable_editing: Enable the editing modes (build, rotate, etc.). - - enable_keys: Enable keyboard shortcuts. - - height: The height of the HTML container. - - never_ask_before_quit: Never display an alert asking if you want to - leave the page. By default, this message is displayed if enable_editing - is True. - - static_site_index_json: The index, as a JSON string, for the static - site. Use javascript to parse the URL options. Used for - generating static pages (see static_site.py). - - protocol: The protocol can be 'http', 'https', or None which indicates a - 'protocol relative URL', as in //escher.github.io. Ignored if source is - local. - - ignore_bootstrap: Deprecated - - """ - - if js_source not in ['web', 'local', 'dev']: - raise Exception('Bad value for js_source: %s' % js_source) - - if menu not in ['none', 'zoom', 'all']: - raise Exception('Bad value for menu: %s' % menu) - - if scroll_behavior not in ['pan', 'zoom', 'none']: - raise Exception('Bad value for scroll_behavior: %s' % scroll_behavior) - - content = env.get_template('content.html') - - # if height is not a string - if type(height) is int: - height = "%dpx" % height - elif type(height) is float: - height = "%fpx" % height - elif type(height) is str: - height = str(height) - - # set the proper urls - url_source = 'local' if (js_source=='local' or js_source=='dev') else 'web' - local_host = self.local_host - - # get the urls - escher_url = get_url(('escher_min' if minified_js else 'escher'), - url_source, local_host, protocol) - favicon_url = get_url('favicon', url_source, local_host, protocol) - # for static site - map_download_url = get_url('map_download', url_source, local_host, protocol) - model_download_url = get_url('model_download', url_source, local_host, protocol) - - # local host - lh_string = ('' if local_host is None else - local_host.rstrip('/') + '/') - - # options - options = { - 'menu': menu, - 'enable_keys': enable_keys, - 'enable_editing': enable_editing, - 'scroll_behavior': scroll_behavior, - 'fill_screen': fill_screen, - 'never_ask_before_quit': never_ask_before_quit, - 'reaction_data': self.reaction_data, - 'metabolite_data': self.metabolite_data, - 'gene_data': self.gene_data, - } - # Add the specified options - for option in self.options: - val = getattr(self, option) - if val is None: - continue - options[option] = val - - html = content.render( - # standalone - title='Escher ' + ('Builder' if enable_editing else 'Viewer'), - favicon_url=favicon_url, - # content - wrapper=html_wrapper, - height=height, - id=self.the_id, - escher_url=escher_url, - # dump json - id_json=b64dump(self.the_id), - options_json=b64dump(options), - map_download_url_json=b64dump(map_download_url), - model_download_url_json=b64dump(model_download_url), - builder_embed_css_json=b64dump(self.embedded_css), - # alreay json - map_data_json=b64dump(self.loaded_map_json), - model_data_json=b64dump(self.loaded_model_json), - static_site_index_json=b64dump(static_site_index_json), - ) - - return html - - - def display_in_notebook(self, js_source='web', menu='zoom', scroll_behavior='none', - minified_js=True, height=500, enable_editing=False): - """Embed the Map within the current IPython Notebook. - - :param string js_source: - - Can be one of the following: - - - *web* (Default) - Use JavaScript files from escher.github.io. - - *local* - Use compiled JavaScript files in the local Escher installation. Works offline. - - *dev* - No longer necessary with source maps. This now gives the - same behavior as 'local'. - - :param string menu: Menu bar options include: - - - *none* - No menu or buttons. - - *zoom* - Just zoom buttons. - - Note: The *all* menu option does not work in an IPython notebook. - - :param string scroll_behavior: Scroll behavior options: - - - *pan* - Pan the map. - - *zoom* - Zoom the map. - - *none* - (Default) No scroll events. - - :param Boolean minified_js: - - If True, use the minified version of JavaScript and CSS files. - - :param height: Height of the HTML container. - - :param Boolean enable_editing: Enable the map editing modes. - - """ - if (enable_editing and menu == 'zoom'): - menu = 'all' - if enable_editing: - print('Some functions (e.g. saving maps) are not available in the notebook. Use ' - 'Builder.display_in_browser() for a full-featured Escher Builder.') - html = self._get_html(js_source=js_source, menu=menu, scroll_behavior=scroll_behavior, - html_wrapper=False, enable_editing=enable_editing, enable_keys=False, - minified_js=minified_js, fill_screen=False, height=height, - never_ask_before_quit=True, ignore_bootstrap=True) - # import here, in case users don't have requirements installed - try: - from IPython.display import HTML - except ImportError: - raise Exception('You need to be using the IPython notebook for this function to work') - return HTML(html) - - - def display_in_browser(self, ip='127.0.0.1', port=7655, n_retries=50, js_source='web', - menu='all', scroll_behavior='pan', enable_editing=True, enable_keys=True, - minified_js=True, never_ask_before_quit=False): - """Launch a web browser to view the map. - - :param ip: The IP address to serve the map on. - - :param port: - - The port to serve the map on. If specified the port is occupied, - then a random free port will be used. - - :param int n_retries: - - The number of times the server will try to find a port before - quitting. - - :param string js_source: - - Can be one of the following: - - - *web* (Default) - Use JavaScript files from escher.github.io. - - *local* - Use compiled JavaScript files in the local Escher installation. Works offline. - - *dev* - No longer necessary with source maps. This now gives the - same behavior as 'local'. - - :param string menu: Menu bar options include: - - - *none* - No menu or buttons. - - *zoom* - Just zoom buttons. - - *all* (Default) - Menu and button bar. - - :param string scroll_behavior: Scroll behavior options: - - - *pan* - Pan the map. - - *zoom* - Zoom the map. - - *none* (Default) - No scroll events. - - :param Boolean enable_editing: Enable the map editing modes. - - :param Boolean enable_keys: Enable keyboard shortcuts. - - :param Boolean minified_js: - - If True, use the minified version of JavaScript and CSS files. - - :param Boolean never_ask_before_quit: - - Never display an alert asking if you want to leave the page. By - default, this message is displayed if enable_editing is True. + # widget info traitlets - """ - html = self._get_html(js_source=js_source, menu=menu, scroll_behavior=scroll_behavior, - html_wrapper=True, enable_editing=enable_editing, enable_keys=enable_keys, - minified_js=minified_js, fill_screen=True, height="100%", - never_ask_before_quit=never_ask_before_quit) - serve_and_open(html, ip=ip, port=port, n_retries=n_retries) + _view_name = Unicode('EscherMapView').tag(sync=True) + _model_name = Unicode('EscherMapModel').tag(sync=True) + _view_module = Unicode('jupyter-escher').tag(sync=True) + _model_module = Unicode('jupyter-escher').tag(sync=True) + _view_module_version = Unicode(__version__).tag(sync=True) + _model_module_version = Unicode(__version__).tag(sync=True) + # editable attributes - def save_html(self, filepath=None, overwrite=False, js_source='web', - protocol='https', menu='all', scroll_behavior='pan', - enable_editing=True, enable_keys=True, minified_js=True, - never_ask_before_quit=False, static_site_index_json=None): - """Save an HTML file containing the map. + height = Int(500).tag(sync=True) + _loaded_map_json = Unicode(None, allow_none=True).tag(sync=True) - :param string filepath: + @observe('_loaded_map_json') + def _observe_loaded_map_json(self, change): + # if map is cleared, then clear these + if not change.new: + self.map_name = None + self.map_json = None - The HTML file will be saved to this location. When js_source is - 'local', then a new directory will be created with this name. + _loaded_model_json = Unicode(None, allow_none=True).tag(sync=True) - :param Boolean overwrite: + @observe('_loaded_model_json') + def _observe_loaded_model_json(self, change): + # if model is cleared, then clear these + if not change.new: + self.model = None + self.model_name = None + self.model_json = None - Overwrite existing files. + embedded_css = Unicode(None, allow_none=True).tag(sync=True) - :param string js_source: - - Can be one of the following: - - - *web* (Default) - Use JavaScript files from escher.github.io. - - *local* - Use compiled JavaScript files in the local Escher - installation. Works offline. To make the dependencies - available to the downloaded file, a new directory will - be made with the name specified by filepath. - - *dev* - No longer necessary with source maps. This now gives the - same behavior as 'local'. - - :param string protocol: - - The protocol can be 'http', 'https', or None which indicates a - 'protocol relative URL', as in //escher.github.io. Ignored if source - is local. - - :param string menu: Menu bar options include: - - - *none* - No menu or buttons. - - *zoom* - Just zoom buttons. - - *all* (Default) - Menu and button bar. - - :param string scroll_behavior: Scroll behavior options: - - - *pan* - Pan the map. - - *zoom* - Zoom the map. - - *none* (Default) - No scroll events. - - :param Boolean enable_editing: Enable the map editing modes. - - :param Boolean enable_keys: Enable keyboard shortcuts. - - :param Boolean minified_js: - - If True, use the minified version of JavaScript and CSS files. - - :param number height: Height of the HTML container. - - :param Boolean never_ask_before_quit: + @validate('embedded_css') + def _validate_embedded_css(self, proposal): + css = proposal['value'] + if css: + return css.replace('\n', '') + else: + return None - Never display an alert asking if you want to leave the page. By - default, this message is displayed if enable_editing is True. + reaction_data = Dict(None, allow_none=True).tag(sync=True) + metabolite_data = Dict(None, allow_none=True).tag(sync=True) + gene_data = Dict(None, allow_none=True).tag(sync=True) + scroll_behavior = Unicode('pan').tag(sync=True) - :param string static_site_index_json: + # builder traitlets that are indirectly synced to the widget - The index, as a JSON string, for the static site. Use javascript - to parse the URL options. Used for generating static pages (see - static_site.py). + map_name = Unicode(None, allow_none=True) + map_json = Unicode(None, allow_none=True) + model = Instance(Model, allow_none=True) + model_name = Unicode(None, allow_none=True) + model_json = Unicode(None, allow_none=True) - """ + @observe('map_name') + def _observe_map_name(self, change): + if change.new: + self._loaded_map_json = map_json_for_name(change.new) + else: + self._loaded_map_json = None - filepath = expanduser(filepath) + @observe('map_json') + def _observe_map_json(self, change): + if change.new: + self._loaded_map_json = _load_resource(change.new, 'map_json') + else: + self._loaded_map_json = None - if js_source in ['local', 'dev']: - if filepath is None: - raise Exception('Must provide a filepath when js_source is not "web"') + @observe('model') + def _observe_model(self, change): + if change.new: + self._loaded_model_json = cobra.io.to_json(change.new) + else: + self._loaded_model_json = None - # make a directory - directory = re.sub(r'\.html$', '', filepath) - if exists(directory): - if not overwrite: - raise Exception('Directory already exists: %s' % directory) - else: - os.makedirs(directory) - # add dependencies to the directory - escher = get_url('escher_min' if minified_js else 'escher', 'local') - favicon = get_url('favicon', 'local') + @observe('model_name') + def _observe_model_name(self, change): + if change.new: + self._loaded_model_json = model_json_for_name(change.new) + else: + self._loaded_model_json = None - for path in [escher, favicon]: - if path is None: - continue - src = join(root_directory, path) - dest = join(directory, path) - dest_dir = dirname(dest) - if not exists(dest_dir): - os.makedirs(dest_dir) - shutil.copy(src, dest) - filepath = join(directory, 'index.html') + @observe('model_json') + def _observe_model_json(self, change): + if change.new: + self._loaded_model_json = _load_resource(change.new, 'model_json') else: - if not filepath.endswith('.html'): - filepath += '.html' - if exists(filepath) and not overwrite: - raise Exception('File already exists: %s' % filepath) - - html = self._get_html(js_source=js_source, menu=menu, - scroll_behavior=scroll_behavior, - html_wrapper=True, enable_editing=enable_editing, - enable_keys=enable_keys, minified_js=minified_js, - fill_screen=True, height="100%", - never_ask_before_quit=never_ask_before_quit, - static_site_index_json=static_site_index_json, - protocol=protocol) - if filepath is not None: - with open(filepath, 'wb') as f: - f.write(html.encode('utf-8')) - return filepath + self._loaded_model_json = None + + def __init__( + self, + # + height: int = 500, + map_name: str = None, + map_json: str = None, + model: Model = None, + model_name: str = None, + model_json: str = None, + embedded_css: str = None, + reaction_data: dict = None, + metabolite_data: dict = None, + gene_data: dict = None, + scroll_behavior: str = 'pan', + # menu: str='zoom', + # enable_editing: bool=False, + # **kwargs, + ) -> None: + super().__init__() + + # set attributes + self.height = height + + if map_json: + if map_name: + warn('map_json overrides map_name') + self.map_json = map_json else: - from tempfile import mkstemp - from os import write, close - os_file, filename = mkstemp(suffix=".html", text=False) # binary - write(os_file, html.encode('utf-8')) - close(os_file) - return filename + self.map_name = map_name + + if model: + if model_name: + warn('model overrides model_name') + if model_json: + warn('model overrides model_json') + self.model = model + elif model_json: + if model_name: + warn('model_json overrides model_name') + self.model_json = model_json + else: + self.model_name = model_name + + self.embedded_css = embedded_css + + self.reaction_data = reaction_data + self.metabolite_data = metabolite_data + self.gene_data = gene_data + self.scroll_behavior = scroll_behavior + + # # set up the options + # self.options = [ + # 'use_3d_transform', + # 'enable_search', + # 'fill_screen', + # 'zoom_to_element', + # 'full_screen_button', + # 'starting_reaction', + # 'unique_map_id', + # 'primary_metabolite_radius', + # 'secondary_metabolite_radius', + # 'marker_radius', + # 'gene_font_size', + # 'hide_secondary_metabolites', + # 'show_gene_reaction_rules', + # 'hide_all_labels', + # 'canvas_size_and_loc', + # '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', + # 'cofactors', + # 'enable_tooltips', + # ] + + # def get_getter_setter(o): + # """Use a closure.""" + # # create local fget and fset functions + # fget = lambda self: getattr(self, '_%s' % o) + # fset = lambda self, value: setattr(self, '_%s' % o, value) + # return fget, fset + # for option in self.options: + # fget, fset = get_getter_setter(option) + # # make the setter + # setattr(self.__class__, 'set_%s' % option, fset) + # # add property to self + # setattr(self.__class__, option, property(fget)) + # # add corresponding local variable + # setattr(self, '_%s' % option, None) + + # # set the kwargs + # for key, val in kwargs.items(): + # try: + # getattr(self, 'set_%s' % key)(val) + # except AttributeError: + # print('Unrecognized keywork argument %s' % key) + + # def display_in_notebook(self): + # """Deprecated. + + # The Builder is now a Jupyter Widget, so you can return the Builder + # object from a cell to display it, or you can manually call the IPython + # display function: + + # from IPython.display import display + # from escher import Builder + # b = Builder(...) + # display(b) + + # """ + # raise Exception(('display_in_notebook is deprecated. The Builder is ' + # 'now a Jupyter Widget, so you can return the ' + # 'Builder in a cell to see it, or use the IPython ' + # 'display function (see Escher docs for details)')) + + # def display_in_browser(self, ip='127.0.0.1', port=7655, n_retries=50, + # js_source='web', menu='all', scroll_behavior='pan', + # enable_editing=True, enable_keys=True, + # minified_js=True, never_ask_before_quit=False): + # """Deprecated. + + # We recommend using the Jupyter Widget (which now supports all Escher + # features) or the save_html option to generate a standalone HTML file + # that loads the map. + + # """ + # raise Exception(('display_in_browser is deprecated. We recommend using' + # 'the Jupyter Widget (which now supports all Escher' + # 'features) or the save_html option to generate a' + # 'standalone HTML file that loads the map.')) + + # def save_html(self, filepath=None): + # """Save an HTML file containing the map. + + # :param string filepath: + + # The name of the HTML file. + + # """ + + # # TODO apply options from self + # options = tranform(self.options) + + # template = env.get_template('standalone.html') + # html = template.render( + # escher_url=get_url('escher_min'), + # # dump json + # options_json=b64dump(options), + # map_download_url_json=b64dump(get_url('map_download')), + # model_download_url_json=b64dump(get_url('model_download')), + # builder_embed_css_json=b64dump(self.embedded_css), + # # alreay json + # map_data_json=b64dump(self.loaded_map_json), + # model_data_json=b64dump(self.loaded_model_json), + # ) + + # with open(expanduser(filepath), 'wb') as f: + # f.write(html.encode('utf-8')) diff --git a/py/escher/quick_server.py b/py/escher/quick_server.py deleted file mode 100644 index a91e5e61..00000000 --- a/py/escher/quick_server.py +++ /dev/null @@ -1,133 +0,0 @@ -""" The following code has been adapted from mpld3. Modifications (c) 2014, -Zachary King. - -mpld3, http://mpld3.github.io/, A Simple server used to show mpld3 images. -Copyright (c) 2013, Jake Vanderplas -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, this - list of conditions and the following disclaimer in the documentation and/or - other materials provided with the distribution. - -* Neither the name of the {organization} nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -""" -import sys -import threading -import webbrowser -import socket -import itertools -import random - -IPYTHON_WARNING = """ -Note: You must interrupt the kernel to end this command -""" - -try: - # Python 2.x - import BaseHTTPServer as server -except ImportError: - # Python 3.x - from http import server - -def generate_handler(html, files=None): - if files is None: - files = {} - - class MyHandler(server.BaseHTTPRequestHandler): - def do_GET(self): - """Respond to a GET request.""" - if self.path == '/': - self.send_response(200) - self.send_header("Content-type", "text/html") - self.end_headers() - self.wfile.write(html.encode('utf-8')) - elif self.path in files: - content_type, content = files[self.path] - self.send_response(200) - self.send_header("Content-type", content_type) - self.end_headers() - self.wfile.write(content) - else: - self.send_error(404) - - return MyHandler - -def find_open_port(ip, port, n=50): - """Find an open port near the specified port""" - ports = itertools.chain((port + i for i in range(n)), - (port + random.randint(-2 * n, 2 * n))) - - for port in ports: - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - result = s.connect_ex((ip, port)) - s.close() - if result != 0: - return port - raise ValueError("no open ports found") - -def serve_and_open(html, ip='127.0.0.1', port=8888, n_retries=50, files=None, - ipython_warning=True): - """Start a server serving the given HTML, and open a browser - - Parameters - ---------- - html : string - HTML to serve - ip : string (default = '127.0.0.1') - ip address at which the HTML will be served. - port : int (default = 8888) - the port at which to serve the HTML - n_retries : int (default = 50) - the number of nearby ports to search if the specified port is in use. - files : dictionary (optional) - dictionary of extra content to serve - ipython_warning : bool (optional) - if True (default), then print a warning if this is used within IPython - """ - port = find_open_port(ip, port, n_retries) - Handler = generate_handler(html, files) - srvr = server.HTTPServer((ip, port), Handler) - - if ipython_warning: - try: - __IPYTHON__ - except: - pass - else: - print(IPYTHON_WARNING) - - # Start the server - print(("Serving to http://{0}:{1}/\n".format(ip, port) + - "[Ctrl-C to exit from terminal, or Ctrl-M i i to interrupt notebook kernel]")) - sys.stdout.flush() - - # Use a thread to open a web browser pointing to the server - b = lambda: webbrowser.open('http://{0}:{1}'.format(ip, port)) - threading.Thread(target=b).start() - - try: - srvr.serve_forever() - except (KeyboardInterrupt, SystemExit): - print("\nstopping Server...") - - srvr.server_close() diff --git a/py/escher/server.py b/py/escher/server.py deleted file mode 100644 index cd599bf2..00000000 --- a/py/escher/server.py +++ /dev/null @@ -1,198 +0,0 @@ -# -*- coding: utf-8 -*- - -from __future__ import print_function, unicode_literals - -from escher.plots import (Builder, local_index, model_json_for_name, - map_json_for_name) -from escher.urls import get_url, root_directory -from escher.util import b64dump - -import os, subprocess -from os.path import join -import tornado.ioloop -from tornado.web import RequestHandler, HTTPError, Application, asynchronous, StaticFileHandler -from tornado.httpclient import AsyncHTTPClient -from tornado import gen -import tornado.escape -from tornado.options import define, options, parse_command_line -import json -import re -from jinja2 import Environment, PackageLoader -from mimetypes import guess_type - -from escher.version import __version__, __schema_version__, __map_model_version__ - -# set up jinja2 template location -env = Environment(loader=PackageLoader('escher', 'templates')) - -# set directory to server -NO_CACHE = False -PORT = 7778 -PUBLIC = False - -def run(port=PORT, public=PUBLIC): - global PORT - global PUBLIC - PORT = port - PUBLIC = public - print('serving directory %s on port %d' % (root_directory, PORT)) - application.listen(port, None if public else "localhost") - try: - tornado.ioloop.IOLoop.instance().start() - except KeyboardInterrupt: - print("bye!") - -def stop(): - tornado.ioloop.IOLoop.instance().stop() - -class BaseHandler(RequestHandler): - def serve_path(self, path): - # make sure the path exists - if not os.path.isfile(path): - raise HTTPError(404) - # serve any raw file type - with open(path, "rb") as file: - data = file.read() - # set the mimetype - the_type = guess_type(path, strict=False)[0] - self.set_header("Content-Type", ("application/octet-stream" - if the_type is None - else the_type)) - self.serve(data) - - def serve(self, data): - if (NO_CACHE): - self.set_header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0') - self.set_header('Access-Control-Allow-Origin', '*') - self.write(data) - self.finish() - -class IndexHandler(BaseHandler): - @asynchronous - @gen.coroutine - def get(self): - # get the organisms, maps, and models - response = yield gen.Task(AsyncHTTPClient().fetch, get_url('server_index', protocol='http')) - if response.code == 200 and response.body is not None: - server_index_json = response.body.decode('utf-8') - else: - server_index_json = None - - # get the cached maps and models - index = local_index() - - # render the template - template = env.get_template('homepage.html') - data = template.render(escher=get_url('escher_min', 'local'), - homepage_css=get_url('homepage_css', 'local'), - favicon=get_url('favicon', 'local'), - logo=get_url('logo', 'local'), - documentation=get_url('documentation', protocol='https'), - github=get_url('github'), - github_releases=get_url('github_releases'), - homepage_js=get_url('homepage_js', 'local'), - map_download_url=get_url('map_download', 'local'), - server_index_json=b64dump(server_index_json), - local_index_json=b64dump(index), - version=__version__, - web_version=False) - - self.set_header("Content-Type", "text/html") - self.serve(data) - -class BuilderHandler(BaseHandler): - @asynchronous - @gen.coroutine - def get(self): - # Builder options - builder_kwargs = {} - for a in ['starting_reaction', 'model_name', 'map_name', 'map_json', - 'reaction_no_data_color', 'reaction_no_data_size', - 'metabolite_no_data_color', 'metabolite_no_data_size', - 'hide_secondary_nodes']: - args = self.get_arguments(a) - if len(args)==1: - builder_kwargs[a] = (True if args[0].lower()=='true' else - (False if args[0].lower()=='false' else - args[0])) - # array args - for a in ['quick_jump', 'metabolite_size_range', 'metabolite_color_range', - 'reaction_size_range', 'reaction_color_range', 'gene_styles']: - args = self.get_arguments(a + '[]') - if len(args) > 0: - builder_kwargs[a] = args - - # js source - args = self.get_arguments('js_source') - js_source = args[0] if len(args) == 1 else 'web' - - # example data - def load_data_file(rel_path): - """Load a JSON file with relative path.""" - try: - with open(join(root_directory, rel_path), 'r') as f: - return json.load(f) - except: - logging.warn('Could not load testing_data file: %s' % rel_path) - if len(self.get_arguments('testing_data')) > 0: - r_filepath = 'escher/testing_data/reaction_data_iJO1366.json' - builder_kwargs['reaction_data'] = load_data_file(r_filepath) - m_filepath = 'escher/testing_data/metabolite_data_iJO1366.json' - builder_kwargs['metabolite_data'] = load_data_file(m_filepath) - - # display options - display_kwargs = {'minified_js': True, - 'scroll_behavior': 'pan', - 'menu': 'all'} - - # keyword - for a in ['menu', 'scroll_behavior', 'minified_js', - 'auto_set_data_domain', 'never_ask_before_quit', - 'enable_editing']: - args = self.get_arguments(a) - if len(args)==1: - display_kwargs[a] = (True if args[0].lower()=='true' else - (False if args[0].lower()=='false' else - args[0])) - - # make the builder - builder = Builder(safe=True, **builder_kwargs) - html = builder._get_html(js_source=js_source, enable_keys=True, - html_wrapper=True, fill_screen=True, - height='100%', **display_kwargs) - - self.set_header("Content-Type", "text/html") - self.serve(html) - -class MapModelHandler(BaseHandler): - def get(self, path): - try: - kind, organism, name = path.strip('/').split('/') - except (TypeError, ValueError): - raise Exception('invalid path %s' % path) - if kind == 'maps': - b = Builder(map_name=name) - self.set_header('Content-Type', 'application/json') - self.serve(b.loaded_map_json) - else: - b = Builder(model_name=name) - self.set_header('Content-Type', 'application/json') - self.serve(b.loaded_model_json) - -settings = {'debug': True} - -application = Application([ - (r'.*escher/static/(.*)', StaticFileHandler, {'path': join(root_directory, 'escher', 'static')}), - (r'/builder/index.html', BuilderHandler), - (r'/%s/%s(/.*)' % (__schema_version__, __map_model_version__), MapModelHandler), - (r'/', IndexHandler), -], **settings) - -if __name__ == '__main__': - # define port - define('port', default=PORT, type=int, help='Port to serve on.') - define('public', default=PUBLIC, type=bool, - help=('If False, listen only on localhost. If True, listen on ' - 'all available addresses.')) - parse_command_line() - run(port=options.port, public=options.public) diff --git a/py/escher/static/.gitignore b/py/escher/static/.gitignore new file mode 100644 index 00000000..c96a04f0 --- /dev/null +++ b/py/escher/static/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/py/escher/static/escher/.gitignore b/py/escher/static/escher/.gitignore deleted file mode 100644 index 86d0cb27..00000000 --- a/py/escher/static/escher/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -# Ignore everything in this directory -* -# Except this file -!.gitignore \ No newline at end of file diff --git a/py/escher/static/homepage/main.css b/py/escher/static/homepage/main.css deleted file mode 100644 index c3c179c0..00000000 --- a/py/escher/static/homepage/main.css +++ /dev/null @@ -1,176 +0,0 @@ -html, -body { - height: 100%; - background-color: #FCFCFC; - /* The html and body elements cannot have any padding or margin. */ -} - -body { - font-size: 17px; - color: #585858; -} - -body, h2, h3, h4, h5, h6 { - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif !important; -} - -h2 { - font-size: 30px !important; -} - -h3 { - font-size: 24px !important; -} - -.form-control { - font-size: 14px !important; -} - -h4 { - font-size: 18px !important; -} - -button { - font-size: 14px !important; -} - -#whats-new { - position: absolute; - top: 5px; - right: 20px; - font-style: italic; - font-size: 18px; - /* background-color: rgba(0, 0, 0, 0.05); */ - border-radius: 2px; - padding: 1px 5px; - border: 1px solid #CF6965; -} -@media (max-width: 768px) { - #whats-new { - font-size: 14px; - } -} - -/* Wrapper for page content to push down footer */ -main { - /* Negative indent footer by it's height */ - margin: 0px auto -60px auto; - max-width: 700px; - min-height: 100%; - height: auto !important; - height: 100%; -} - -/* Demos */ -.demo-title { - position: absolute; - margin: 5px 0 0 20px; - color: #5A5A5A; - font-style: italic; - font-size: 32px; - font-weight: bold; - z-index: 10; -} -.demo-img { - border: 3px solid #eee; - opacity: 0.7; -} - -/* Set the fixed height of the footer here */ -#push { - height: 80px; -} -footer { - height: 60px; - background-color: #291E1E; - color: #eee; -} -a, a:hover, a:focus, a:visited { - color: #CF6965; -} -footer a, footer a:hover, footer a:focus, footer a:visited { - color: #FFA09D; -} -@media (max-width: 768px) { - #footer { - margin-left: -20px; - margin-right: -20px; - padding-left: 20px; - padding-right: 20px; - } -} - -.container { - width: auto; - max-width: 700px; -} -.container .more { - margin: 20px 0; - float: left; -} -.container .version { - margin: 20px 0; - float: right; -} -.column-button { - width: 100%; - margin-top: 35px; -} -label { - font-weight: inherit; -} -#title-box { - display: inline-block; -} -#title { - color: #291E1E; - font-size: 160px; - font-family: tulpen-one, Futura, serif; - font-weight: 400; - margin-bottom: 0px; -} -#title-box p { - font-size: 22px; - font-style: italic; -} -#logo { - vertical-align: inherit; - margin-bottom: 60px; -} -#faq li { - padding-top: 5px; -} -@media (max-width: 768px) { - #title-box { - width: 100%; - text-align: center; - } - #logo { - display: none; - } -} -@media (min-width: 768px) { /*@screen-sm-min) { */ - .column-button { - margin-top: 56px; - } - header { - margin-bottom: 20px; - } - #title-box { - max-width: 400px; - overflow: hidden; - } - #title { - position: relative; - top: 45px; - left: -7px; - font-size: 230px; - text-align: left; - margin-bottom: 10px; - } - #logo { - width: 250px; - display: inline-block; - overflow: hidden; - } -} diff --git a/py/escher/static/homepage/main.js b/py/escher/static/homepage/main.js deleted file mode 100644 index 593da862..00000000 --- a/py/escher/static/homepage/main.js +++ /dev/null @@ -1,380 +0,0 @@ -/* global escher */ - -var MAP_DOWNLOAD -var SERVER_INDEX -var LOCAL_INDEX - -/** - * Get model from a map name. - */ -function get_model (name) { - var parts = name.split('.') - if (parts.length == 2) - return parts[0] - return null -} - -function select_model (model_name) { - var sel = escher.libs.d3_select('#models') - var ind = 0 - sel.selectAll('option') - .each(function (d, i) { - if (d && (d.model_name === model_name)) - ind = i - }) - sel.node().selectedIndex = ind -} - -/** - * Find maps with the same model. Returns null if no quick jump options could be - * found. - */ -function get_quick_jump (this_map, server_index, local_index) { - var model = get_model(this_map) - if (model === null) { - return null - } - - var quick_jump = {} - var all_maps = [] - if (server_index !== null) { - all_maps = all_maps.concat(server_index.maps) - } - if (local_index !== null) { - all_maps = all_maps.concat(local_index.maps) - } - all_maps.forEach(function (o) { - if (get_model(o.map_name) === model) { - quick_jump[o.map_name] = true - } - }) - quick_jump = Object.keys(quick_jump) - return quick_jump.length == 0 ? null : quick_jump -} - -function submit (server_index, local_index, map_download) { - // get the selections - var maps = escher.libs.d3_select('#maps') - var map_data = (maps.selectAll('option') - .filter(function(d, i) { - return i == maps.node().selectedIndex - }).node().__data__) - var map_name = map_data ? map_data.map_name : null - var organism = map_data ? map_data.organism : null - var model_name = escher.libs.d3_select('#models').node().value - var options_value = escher.libs.d3_select('#tools').node().value - var scroll_value = escher.libs.d3_select('#scroll').node().checked - var never_ask_value = escher.libs.d3_select('#never_ask').node().checked - var add = [] - var url - // only add model for builder - if (model_name !== 'none' && options_value === 'builder') { - add.push('model_name=' + model_name) - } - if (map_name !== null) { - add.push('map_name=' + map_name) - } - if (scroll_value) { - add.push('scroll_behavior=zoom') - } - if (never_ask_value) { - add.push('never_ask_before_quit=true') - } - if (options_value === 'viewer') { - add.push('enable_editing=false') - } else { - add.push('enable_editing=true') - } - - // choose the file - url = 'builder/index.html' - add.push('js_source=local') - - // set the quick jump maps - if (map_name) { - var quick_jump = get_quick_jump(map_name, server_index, local_index) - if (quick_jump !== null) { - quick_jump.forEach(function(o) { - add.push('quick_jump[]=' + o) - }) - add.push('quick_jump_path=' + map_download + encodeURIComponent(organism)) - } - } - - url += '?' - for (var i=0, l=add.length; i < l; i++) { - if (i > 0) url += '&' - url += add[i] - } - window.open(url, '_blank') -} - -/** - * Draw the models selector. - */ -function draw_models_select (server_index, local_index) { - // filter function - var filter_models = function(d) { - var org = escher.libs.d3_select('#organisms').node().value - if (org === 'all') { - return true - } - if (org === d.organism) { - return true - } - return false - } - - var web_sel - var local_sel - var select_sel = escher.libs.d3_select('#models') - if (local_index === null) { - // no cache - web_sel = select_sel - } else { - // local cache - var s = select_sel.selectAll('optgroup') - .data([ [ 'local', 'Cached' ], [ 'web', 'Web' ] ]) - s.enter() - .append('optgroup') - .merge(s) - .attr('label', function (d) { return d[1] }) - .attr('id', function (d) { return 'models-' + d[0] }) - web_sel = select_sel.select('#models-web') - local_sel = select_sel.select('#models-local') - } - - // cached - var local_model_names = [] - var model_data - var map_data - if (local_index !== null) { - model_data = local_index.models.filter(filter_models) - local_model_names = model_data.map(function(x) { return x.model_name; }) - - var models_sel = local_sel.selectAll('.model') - .data(model_data, function (d) { return d.model_name }) - models_sel.enter() - .append('option') - .classed('model', true) - .merge(models_sel) - .attr('value', function (d) { return d.model_name }) - .text(function(d) { - var parts = d.model_name.split('.') - if (parts.length==2) { - return d.model_name.split('.').slice(-1)[0] - } else { - return d.model_name - } - }) - models_sel.exit().remove() - } - - // web - if (server_index !== null) { - model_data = server_index.models - .filter(filter_models) - .filter(function(o) { return local_model_names.indexOf(o.model_name) == -1; }) - models_sel = web_sel.selectAll('.model') - .data(model_data, function(d) { return d.model_name; }) - models_sel.enter() - .append('option') - .classed('model', true) - .merge(models_sel) - .attr('value', function(d) { return d.model_name; }) - .text(function(d) { - return d.model_name.split('.').slice(-1)[0] - }) - models_sel.exit().remove() - } - -} - -/** - * Draw the models selector. - * @param {Object} server_index - The index for the server. - * @param {Object} local_index - The index of local maps and models - * @return {Boolean} True if there are maps, and false if no maps. - */ -function draw_maps_select (server_index, local_index) { - var filter_maps = function(d) { - var org = escher.libs.d3_select('#organisms').node().value - if (org === 'all') { - return true - } - if (org === d.organism) { - return true - } - return false - } - - var select_sel = escher.libs.d3_select('#maps') - if (local_index === null) { - // no cache - web_sel = select_sel - } else { - // local cache - var s = select_sel.selectAll('optgroup') - .data([ [ 'local', 'Cached' ], [ 'web', 'Web' ] ]) - s.enter() - .append('optgroup') - .merge(s) - .attr('label', function (d) { return d[1] }) - .attr('id', function (d) { return 'maps-' + d[0] }) - var web_sel = escher.libs.d3_select('#maps-web') - var local_sel = escher.libs.d3_select('#maps-local') - } - - // cached - var local_map_names = [] - var has_maps = false - var map_data - if (local_index !== null) { - map_data = local_index.maps.filter(filter_maps) - local_map_names = map_data.map(function(x) { return x.map_name; }) - if (map_data.length > 0) - has_maps = true - - var maps_sel = local_sel.selectAll('.map') - .data(map_data, function(d) { return d.map_name; }) - maps_sel.enter() - .append('option') - .classed('map', true) - .merge(maps_sel) - .text(function (d) { - var parts = d.map_name.split('.') - if (parts.length == 2) { - var map = d.map_name.split('.').slice(-1)[0] - var model = get_model(d.map_name) - return map + ' (' + model + ')' - } else { - return d.map_name - } - }) - maps_sel.exit().remove() - } - - // web - if (server_index !== null) { - map_data = server_index.maps - .filter(filter_maps) - .filter(function(o) { return local_map_names.indexOf(o.map_name) == -1; }) - if (map_data.length > 0) - has_maps = true - - maps_sel = web_sel.selectAll('.map') - .data(map_data, function (d) { return d.map_name }) - maps_sel.enter() - .append('option') - .classed('map', true) - .merge(maps_sel) - .text(function(d) { - var map = d.map_name.split('.').slice(-1)[0] - var model = get_model(d.map_name) - return map + ' (' + model + ')' - }) - maps_sel.exit().remove() - } - - // select the first map and model - if (has_maps && select_sel.node().selectedIndex == 0) { - select_sel.node().selectedIndex = 1 - select_sel - .selectAll('option') - .each(function(d, i) { - if (i == 1) select_model(get_model(d.map_name)) - }) - } - - return has_maps -} - -function draw_organisms_select (organisms) { - var org = escher.libs.d3_select('#organisms').selectAll('.organism') - .data(organisms, function (d) { return d }) - org.enter() - .append('option') - .classed('organism', true) - .merge(org) - .attr('value', function (d) { return d }) - .text(function (d) { return d }) -} - -function setup (server_index, local_index, map_download) { - // GO - var uniq = function (a) { - var seen = {} - return a.filter(function (item) { - return seen.hasOwnProperty(item) ? false : (seen[item] = true) - }) - } - var not_cached = function (web, local) { - if (local === null) return web - return web.filter(function (m) { - return local.indexOf(m) === -1 - }) - } - - // organisms - var organisms = {} - ;[ local_index, server_index ] - .filter(function (x) { return x !== null; }) - .forEach(function (i) { - ;[ 'maps', 'models' ].forEach(function (n) { - i[n].forEach(function (m) { - organisms[m.organism] = true - }) - }) - }) - organisms = Object.keys(organisms) - - // draw dropdown menus - draw_organisms_select(organisms) - draw_models_select(server_index, local_index) - var has_maps = draw_maps_select(server_index, local_index) - - // update filters - escher.libs.d3_select('#organisms') - .on('change', function() { - draw_models_select(server_index, local_index) - draw_maps_select(server_index, local_index) - }) - - // select an appropriate model for selected map - escher.libs.d3_select('#maps') - .on('change', function() { - var is_none = this.value === 'none' - var selectedIndex = this.selectedIndex - escher.libs.d3_select(this) - .selectAll('option') - .each(function (d, i) { - if (d && i == selectedIndex) - select_model(get_model(d.map_name)) - }) - }) - - // disable Model for viewer - escher.libs.d3_select('#tools') - .on('change', function() { - escher.libs.d3_select('#models') - .attr('disabled', this.value.indexOf('viewer') == -1 ? null : true) - }) - - // select the first map - var map_node = escher.libs.d3_select('#maps').node() - if (has_maps && map_node.selectedIndex == 0) - map_node.selectedIndex = 1 - - // submit button - escher.libs.d3_select('#submit') - .on('click', submit.bind(null, server_index, local_index, map_download)) - - // submit on enter - var selection = escher.libs.d3_select(window) - var kc = 13 - selection.on('keydown.' + kc, function () { - if (escher.libs.d3_selection.event.keyCode === kc) { - submit(server_index, local_index, map_download) - } - }) -} diff --git a/py/escher/static/img/escher-logo.png b/py/escher/static/img/escher-logo.png deleted file mode 100644 index 150f1818..00000000 Binary files a/py/escher/static/img/escher-logo.png and /dev/null differ diff --git a/py/escher/static/img/escher-logo@2x.png b/py/escher/static/img/escher-logo@2x.png deleted file mode 100644 index c84065cd..00000000 Binary files a/py/escher/static/img/escher-logo@2x.png and /dev/null differ diff --git a/py/escher/static/img/favicon.ico b/py/escher/static/img/favicon.ico deleted file mode 100644 index b79bc3d1..00000000 Binary files a/py/escher/static/img/favicon.ico and /dev/null differ diff --git a/py/escher/static/img/knockout-demo-screenshot.png b/py/escher/static/img/knockout-demo-screenshot.png deleted file mode 100644 index 4e022005..00000000 Binary files a/py/escher/static/img/knockout-demo-screenshot.png and /dev/null differ diff --git a/py/escher/static/img/sbrg-logo.png b/py/escher/static/img/sbrg-logo.png deleted file mode 100644 index 1613f98d..00000000 Binary files a/py/escher/static/img/sbrg-logo.png and /dev/null differ diff --git a/py/escher/static/img/structures-demo-screenshot.png b/py/escher/static/img/structures-demo-screenshot.png deleted file mode 100644 index da564046..00000000 Binary files a/py/escher/static/img/structures-demo-screenshot.png and /dev/null differ diff --git a/py/escher/static/jsonschema/.gitignore b/py/escher/static/jsonschema/.gitignore deleted file mode 100644 index 86d0cb27..00000000 --- a/py/escher/static/jsonschema/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -# Ignore everything in this directory -* -# Except this file -!.gitignore \ No newline at end of file diff --git a/py/escher/static_site.py b/py/escher/static_site.py deleted file mode 100644 index 05419de8..00000000 --- a/py/escher/static_site.py +++ /dev/null @@ -1,60 +0,0 @@ -from __future__ import print_function, unicode_literals - -from escher.plots import Builder -from escher.urls import get_url -from escher.version import __version__ -from escher.urls import top_directory, root_directory - -from os.path import join, dirname, realpath -from jinja2 import Environment, PackageLoader -import shutil - -# set up jinja2 template location -env = Environment(loader=PackageLoader('escher', 'templates')) - -def generate_static_site(): - print('Generating static site at %s' % top_directory) - - # index file - template = env.get_template('homepage.html') - - def static_rel(path): - return 'py/' + path - - data = template.render(escher=static_rel(get_url('escher_min', 'local')), - homepage_css=static_rel(get_url('homepage_css', 'local')), - favicon=static_rel(get_url('favicon', 'local')), - logo=static_rel(get_url('logo', 'local')), - documentation=get_url('documentation', protocol='https'), - github=get_url('github', protocol='https'), - github_releases=get_url('github_releases', protocol='https'), - homepage_js=static_rel(get_url('homepage_js', 'local')), - version=__version__, - map_download_url=get_url('map_download', 'local'), - web_version=True, - server_index_url=static_rel(get_url('server_index', 'local'))) - - with open(join(top_directory, 'index.html'), 'wb') as f: - f.write(data.encode('utf-8')) - - # viewer and builder - # make the builder - builder = Builder(safe=True, id='static_map') - - filepath = join(top_directory, 'builder') - with open(join(root_directory, get_url('server_index', source='local')), 'r') as f: - index_json = f.read() - html = builder.save_html(filepath=filepath, - overwrite=True, - js_source='local', - protocol=None, - minified_js=True, - static_site_index_json=index_json) - - # copy over the source map - escher_map = get_url('escher_min', 'local') + '.map' - shutil.copy(join(root_directory, escher_map), join(top_directory, 'builder', escher_map)) - - -if __name__ == '__main__': - generate_static_site() diff --git a/py/escher/templates/content.html b/py/escher/templates/content.html deleted file mode 100644 index a6b49e4a..00000000 --- a/py/escher/templates/content.html +++ /dev/null @@ -1,98 +0,0 @@ -{% if wrapper %}{% extends 'standalone.html' %}{% endif %} - -{% block content %} -
- - -{% endblock %} diff --git a/py/escher/templates/homepage.html b/py/escher/templates/homepage.html deleted file mode 100644 index 663bae6f..00000000 --- a/py/escher/templates/homepage.html +++ /dev/null @@ -1,210 +0,0 @@ -{% extends "standalone.html" %} - -{% block title %}Escher{% endblock %} - -{% block head %} - - - - - - - - - -{% endblock %} - -{% block content %} -
-
-
-
- - -
-

ESCHER

-

Build, share, and embed visualizations of biological pathways.

-
-
-
-
-
-

Filter by organism

- -
-
-
-
-

Map

- -
-
-

Model (Optional)

- -
-
-

Tool

- -
-
-
-
-

Options

-
- -
-
- -
-
-
- -
-
- -
- - {% if web_version %} - -
-
-

Demos

-
- - -
- -
- - - -
-
-

EscherConverter

-
-
-
-

EscherConverter is a standalone program that reads files - created with Escher and converts them to files in community - standard formats.

-

Learn more and download.

-
-
-
- -
- - {% endif %} - - -
-
-

FAQ

-
    -
  1. What is Escher?

    -

    Escher is a web-based tool for building, viewing, and sharing - visualizations of biological pathways. These 'pathway maps' are a - great way to contextualize data about metabolism. To get started, - load a map by clicking the Load Map button above, or visit - the documentation to learn more. -

    -
  2. -
  3. How do I cite Escher?

    -

    You can help support Escher by citing our publication when you - use Escher or EscherConverter:

    -

    Zachary A. King, Andreas Dräger, Ali Ebrahim, Nikolaus - Sonnenschein, Nathan E. Lewis, and Bernhard O. Palsson - (2015) Escher: A web application for building, sharing, and - embedding data-rich visualizations of biological pathways, - PLOS Computational Biology 11(8): e1004321. - doi:10.1371/journal.pcbi.1004321

    -
  4. Are there more maps available? Can I contribute maps?

    -

    We will be uploading maps for all of the organisms in the - BiGG database. If you - would like to contribute maps, there is a - guide - available in the documentation. -

  5. -
  6. How is Escher licensed?

    -

    Escher and EscherConverter are distributed under the - MIT license.

    -
  7. What browsers can I use?

    -

    We recommend using Google Chrome for optimal performance, but - Escher will also run in the latest versions of Firefox, Internet - Explorer, and Safari (including mobile Safari).

    -
  8. -
  9. I have more questions. Who do I ask?

    -

    Visit the documentation - to get started with Escher and explore the API, or ask - questions in the Gitter chat - room. If you find bugs or would like to contribute to - the project, feel free to submit an issue and or a pull - request on Github.

    -
  10. -
-
- -
-
-
-
- - - - -{% endblock %} diff --git a/py/escher/templates/standalone.html b/py/escher/templates/standalone.html index 98ea2066..796958e5 100644 --- a/py/escher/templates/standalone.html +++ b/py/escher/templates/standalone.html @@ -1,19 +1,41 @@ - {% block title %}{{ title }}{% endblock %} + Escher Builder - {% block head %}{% endblock %} - - {% if favicon_url %} - - {% endif %} + + initial-scale=1.0, maximum-scale=1.0, user-scalable=no, minimal-ui"/> - {% block content %}{% endblock %} +
+ + diff --git a/py/escher/tests/test_convert_map.py b/py/escher/tests/test_convert_map.py deleted file mode 100644 index ab7ca76d..00000000 --- a/py/escher/tests/test_convert_map.py +++ /dev/null @@ -1,249 +0,0 @@ -from __future__ import print_function, unicode_literals - -from escher.convert_map import * -from escher.urls import root_directory -import cobra.io -from os.path import join -from pytest import raises - -def get_old_map(): - # map - return {'reactions': {'1': {'bigg_id': 'GAPD', - 'label_x': '0', - 'label_y': 0, - "metabolites": [{ - "coefficient": -1, - "bigg_id": "g3p_c" - }], - 'segments': {'2': {'from_node_id': '0', - 'to_node_id': '1', - 'b1': None, - 'b2': {'x': None, 'y': None}}, - '10': {'from_node_id': '11', - 'to_node_id': '12'}}}, - '2': {'bigg_id': 'PDH'}, - '3': {'bigg_id': 'TTT', - 'label_x': 0, - 'label_y': 0, - "metabolites": [ - { - "coefficient": -1, - "bigg_id": "test_met_c" - }, - { - "coefficient": -1, - "bigg_id": "glc__D_c" - } - ], - 'segments': { - '10': {'from_node_id': '3', - 'to_node_id': '4'} - } - }}, - 'nodes': {'0': {'node_type': 'multimarker', - 'x': 1, - 'y': 2}, - '1': {'node_type': 'metabolite', - 'label_x': 10, - 'label_y': 12.2, - 'x': 1, - 'y': 2, - 'bigg_id': 'g3p_c', - 'name': 'glycerol-3-phosphate'}, - '3': {'node_type': 'metabolite', - 'x': 1, - 'y': 2, - 'bigg_id': 'glc__D_c', - 'name': 'D-Glucose'}, - '4': {'node_type': 'multimarker', - 'x': 9, - 'y': 9}, - }, - 'text_labels': []} # should be {} - -def get_new_map(): - return [ - {"schema": "https://escher.github.io/escher/jsonschema/1-0-0#", - "homepage": "https://escher.github.io", - "map_name": "", - "map_id": "", - "map_description": "my new map"}, - {"reactions": {"1576769": {"name": "glyceraldehyde-3-phosphate dehydrogenase", - "bigg_id": "GAPD", - "reversibility": True, - "label_x": 1065, - "label_y": 2385, - "gene_reaction_rule": "b1779", - "genes": [{"bigg_id": "b1779", "name": "b1779"}], - "metabolites": [ - {"coefficient": 1, "bigg_id": "nadh_c"}, - {"coefficient": 1, "bigg_id": "13dpg_c"}, - {"coefficient": -1, "bigg_id": "pi_c"}, - {"coefficient": 1, "bigg_id": "h_c"}, - {"coefficient": -1, "bigg_id": "nad_c"}, - {"coefficient": -1, "bigg_id": "g3p_c"} - ], - "segments": { - "217": {"from_node_id": "1577006", "to_node_id": "1577008", "b1": None, "b2": None}, - "218": {"from_node_id": "1577008", "to_node_id": "1577007", "b1": None, "b2": None}, - "219": {"from_node_id": "1576575", "to_node_id": "1577006", "b1": {"y": 2270, "x": 1055}, "b2": {"y": 2322.5, "x": 1055}}, - "220": {"from_node_id": "1576669", "to_node_id": "1577006", "b1": {"y": 2284.7920271060384, "x": 1055}, "b2": {"y": 2326.9376081318114, "x": 1055}}, - "221": {"from_node_id": "1576670", "to_node_id": "1577006", "b1": {"y": 2297.5658350974745, "x": 1055}, "b2": {"y": 2330.769750529242, "x": 1055}}, - "222": {"from_node_id": "1577007", "to_node_id": "1576487", "b1": {"y": 2454.5, "x": 1055}, "b2": {"y": 2500, "x": 1055}}, - "223": {"from_node_id": "1577007", "to_node_id": "1576671", "b1": {"y": 2453.0623918681886, "x": 1055}, "b2": {"y": 2495.2079728939616, "x": 1055}}, - "224": {"from_node_id": "1577007", "to_node_id": "1576672", "b1": {"y": 2449.230249470758, "x": 1055}, "b2": {"y": 2482.4341649025255, "x": 1055}} - } - } - }, - "nodes": { - "1577006": {"node_type": "multimarker", "x": 1055, "y": 2345}, - "1577007": {"node_type": "multimarker", "x": 1055, "y": 2435}, - "1577008": {"node_type": "midmarker", "x": 1055, "y": 2395}, - "1576575": {"node_type": "metabolite", "x": 1055, "y": 2195, "bigg_id": "g3p_c", "name": "Glyceraldehyde-3-phosphate", "label_x": 1085, "label_y": 2195, "node_is_primary": True}, - "1576487": {"node_type": "metabolite", "x": 1055, "y": 2565, "bigg_id": "13dpg_c", "name": "3-Phospho-D-glyceroyl-phosphate", "label_x": 1085, "label_y": 2565, "node_is_primary": True}, - "1576669": {"node_type": "metabolite", "x": 1145, "y": 2265, "bigg_id": "nad_c", "name": "Nicotinamide-adenine-dinucleotide", "label_x": 1165, "label_y": 2265, "node_is_primary": False}, - "1576670": {"node_type": "metabolite", "x": 1145, "y": 2315, "bigg_id": "pi_c", "name": "Phosphate", "label_x": 1165, "label_y": 2315, "node_is_primary": False}, - "1576671": {"node_type": "metabolite", "x": 1145, "y": 2515, "bigg_id": "nadh_c", "name": "Nicotinamide-adenine-dinucleotide-reduced", "label_x": 1165, "label_y": 2515, "node_is_primary": False}, - "1576672": {"node_type": "metabolite", "x": 1145, "y": 2465, "bigg_id": "h_c", "name": "H", "label_x": 1169, "label_y": 2465, "node_is_primary": False}, - } - } - ] - - -def test_get_header(): - assert get_header([1,2]) == 1 - -def test_get_body(): - assert get_body([1,2]) == 2 - -def test_is_valid_body(): - assert is_valid_body(get_old_map()) - -def test_dict_with_required_elements(): - out = {'my': 'dict', 'remove': 'me'} - dict_with_required_elements(out, ['my', 'also'], - get_default=lambda x, _: 'this' if x == 'also' else None) - assert out['also'] == 'this' - assert 'my' in out - assert out['my'] is out['my'] - assert 'remove' not in out - - with raises(MissingDefaultAttribute): - out = {'my': 'dict'} - dict_with_required_elements(out, ['also']) - - # nullable attributes - with raises(MissingDefaultAttribute): - out = {'my': None} - dict_with_required_elements(out, ['my']) - out = {'my': None} - dict_with_required_elements(out, ['my'], nullable=['my']) - - # casting - out = {'my': 0} - dict_with_required_elements(out, ['my'], cast={'my': float}) - assert out['my'] == 0 - -def test_old_map_to_new_schema(): - old_map = get_old_map() - new_map = old_map_to_new_schema(old_map) - assert len(new_map) == 2 - assert 'schema' in get_header(new_map) - assert 'reactions' in get_body(new_map) - assert old_map['reactions'] is get_reactions(get_body(new_map)) - # fixed bezier attribute - assert get_body(new_map)['reactions']['1']['segments']['2']['b2'] is None - assert get_body(new_map)['canvas']['x'] == -1440 - assert get_body(new_map)['text_labels'] == {} - # removed reaction with missing metabolite - assert '3' not in get_body(new_map)['reactions'] - -def test_old_map_to_new_schema__map_name(): - """Make sure the arguments are being used.""" - new_map = get_new_map() - converted = old_map_to_new_schema(new_map, map_name='Tosche', - map_description='Station') - assert converted[0]['map_name'] == 'Tosche' - assert converted[0]['map_description'] == 'Station' - new_map = get_new_map() - converted = old_map_to_new_schema(new_map, map_name='Tosche', - map_description=None) - assert converted[0]['map_name'] == 'Tosche' - assert converted[0]['map_description'] == 'my new map' - new_map = get_new_map() - converted = old_map_to_new_schema(new_map, map_name=None, - map_description='Station') - assert converted[0]['map_name'] == '' - assert converted[0]['map_description'] == 'Station' - -def test_apply_id_mappings(): - new_map = get_new_map() - reaction_id_mapping = {'GAPD': 'GAPD_1'} - metabolite_id_mapping = {'g3p_c': 'g3p_c_1'} - gene_id_mapping = {'b1779': 'gapA'} - apply_id_mappings(new_map, reaction_id_mapping, metabolite_id_mapping, gene_id_mapping) - assert list(get_reactions(get_body(new_map)).values())[0]['bigg_id'] == 'GAPD_1' - assert list(get_reactions(get_body(new_map)).values())[0]['genes'][0]['bigg_id'] == 'gapA' - assert 'g3p_c_1' in [x['bigg_id'] for x in list(get_reactions(get_body(new_map)).values())[0]['metabolites']] - assert 'g3p_c_1' in [x['bigg_id'] for x in get_nodes(get_body(new_map)).values() if 'bigg_id' in x] - -def test_apply_cobra_model_to_map(): - new_map = get_new_map() - model = cobra.io.load_json_model(join(root_directory, 'escher', 'testing_data', 'iJO1366.json')) - model.reactions.get_by_id('GAPD').gene_reaction_rule = '12345' - model.reactions.get_by_id('GAPD').name = '54321' - assert model.reactions.get_by_id('GAPD').lower_bound < 0 - model.reactions.get_by_id('GAPD').lower_bound = 0 - model.genes.get_by_id('12345').name = 'gonzo' - apply_cobra_model_to_map(new_map, model) - gapd_reaction = list(get_reactions(get_body(new_map)).values())[0] - assert gapd_reaction['gene_reaction_rule'] == model.reactions.get_by_id('GAPD').gene_reaction_rule - assert gapd_reaction['name'] == model.reactions.get_by_id('GAPD').name - assert gapd_reaction['reversibility'] is False - assert 'gonzo' in [x['name'] for x in gapd_reaction['genes']] - - -def test_convert(): - model = cobra.io.load_json_model(join(root_directory, 'escher', 'testing_data', 'iJO1366.json')) - - # reverse the reaction - model.reactions.get_by_id('GAPD').upper_bound = 0 - model.reactions.get_by_id('GAPD').lower_bound = -1000 - - new_map = convert(get_old_map(), model) - - # no segments: delete reaction - assert '2' not in new_map[1]['reactions'] - - # missing segments: delete reaction - assert '1' not in new_map[1]['reactions'] - - # now again with the correct number of segments - gapd = model.reactions.get_by_id('GAPD') - gapd.subtract_metabolites({k: v for k, v in gapd.metabolites.items() if k.id != 'g3p_c'}) - new_map = convert(get_old_map(), model) - - # reversed the map - for m in new_map[1]['reactions']['1']['metabolites']: - if m['bigg_id'] == 'g3p_c': - assert m['coefficient'] == 1 - elif m['bigg_id'] == 'nadh_c': - assert m['coefficient'] == -1 - assert new_map[1]['reactions']['1']['reversibility'] == False - - # Remove unconnected nodes. These do not make the map invalid, but they are - # annoying. - assert '3' not in new_map[1]['nodes'] - - # casting - assert type(new_map[1]['reactions']['1']['label_x']) is float - assert new_map[1]['reactions']['1']['segments']['2']['b1'] is None - - -def test_convert_2(): - model = cobra.io.load_json_model(join(root_directory, 'escher', 'testing_data', 'iJO1366.json')) - convert(get_new_map(), model) - - -def test_genes_for_gene_reaction_rule(): - assert genes_for_gene_reaction_rule('((G1 and G2)or G3and)') == ['G1', 'G2', 'G3and'] diff --git a/py/escher/tests/test_generate_index.py b/py/escher/tests/test_generate_index.py deleted file mode 100644 index b77fda82..00000000 --- a/py/escher/tests/test_generate_index.py +++ /dev/null @@ -1,23 +0,0 @@ -from __future__ import print_function, unicode_literals - -from escher.generate_index import generate_index -from os.path import join -from os import makedirs -from shutil import rmtree -import pytest - -@pytest.fixture() -def index_dir(request, tmpdir): - tmpdir.mkdir('maps').mkdir('Escherichia coli').join('iJO1366.central_metabolism.json').write('temp') - tmpdir.mkdir('models').mkdir('Escherichia coli').join('iJO1366.json').write('temp') - def fin(): - tmpdir.remove() - request.addfinalizer(fin) - return str(tmpdir) - -def test_generate_index(index_dir): - out = generate_index(index_dir) - assert out['models'] == [ { 'organism': 'Escherichia coli', - 'model_name': 'iJO1366' } ] - assert out['maps'] == [ { 'organism': 'Escherichia coli', - 'map_name': 'iJO1366.central_metabolism' } ] diff --git a/py/escher/tests/test_plots.py b/py/escher/tests/test_plots.py index 51433607..9a4f608e 100644 --- a/py/escher/tests/test_plots.py +++ b/py/escher/tests/test_plots.py @@ -1,11 +1,14 @@ +# -*- coding: utf-8 -*- from __future__ import print_function, unicode_literals from escher import __schema_version__, __map_model_version__ -import escher.server -from escher import Builder, get_cache_dir, clear_cache -from escher.plots import (_load_resource, local_index, server_index, - model_json_for_name, map_json_for_name) - +from escher import Builder +from escher.plots import ( + _load_resource, + server_index, + model_json_for_name, + map_json_for_name, +) from escher.urls import get_url import base64 @@ -24,46 +27,6 @@ else: unicode_type = str -# cache - -def test_get_cache_dir(): - d = get_cache_dir(versioned=False) - assert os.path.isdir(d) - assert basename(d) != __map_model_version__ - d = get_cache_dir() - assert os.path.isdir(d) - assert basename(d) == __map_model_version__ - assert __schema_version__ in d - d = get_cache_dir(name='maps') - assert os.path.isdir(d) - -def test_clear_cache(tmpdir, request): - (tmpdir.mkdir('2').mkdir('maps').mkdir('Escherichia coli') - .join('iJO1366.Central metabolism.json').write('temp')) - (tmpdir.join('2').mkdir('models').mkdir('Escherichia coli') - .join('iJO1366.json').write('temp')) - (tmpdir.mkdir('x').mkdir('y').mkdir('z')) - clear_cache(str(tmpdir), ask=False) - assert os.listdir(str(tmpdir)) == [] - def fin(): - tmpdir.remove() - request.addfinalizer(fin) - -def test_local_index(tmpdir, request): - maps = tmpdir.mkdir('maps') - maps.mkdir('Escherichia coli').join('iJO1366.Central metabolism.json').write('temp') - # ignore these - maps.join('ignore_md.json').write('ignore') - tmpdir.mkdir('models').mkdir('Escherichia coli').join('iJO1366.json').write('temp') - assert local_index(str(tmpdir)) == { 'maps': [ { 'organism': 'Escherichia coli', - 'map_name': 'iJO1366.Central metabolism' } ], - 'models': [ { 'organism': 'Escherichia coli', - 'model_name': 'iJO1366' } ] } - def fin(): - tmpdir.remove() - request.addfinalizer(fin) - -# server @mark.web def test_server_index(): @@ -75,47 +38,26 @@ def test_server_index(): assert 'organism' in model_0 assert 'model_name' in model_0 -# model and maps - -def test_model_json_for_name(tmpdir): - models = tmpdir.mkdir('models') - models.mkdir('Escherichia coli').join('iJO1366.json').write('"temp"') - json = model_json_for_name('iJO1366', cache_dir=str(tmpdir)) - assert json == '"temp"' - -@mark.web -def test_model_json_for_name_web(tmpdir): - data = model_json_for_name('iJO1366', cache_dir=str(tmpdir)) - assert 'reactions' in data - assert 'metabolites' in data - -def test_map_json_for_name(tmpdir): - maps = tmpdir.mkdir('maps') - maps.mkdir('Escherichia coli').join('iJO1366.Central metabolism.json').write('"temp"') - json = map_json_for_name('iJO1366.Central metabolism', cache_dir=str(tmpdir)) - assert json == '"temp"' - -@mark.web -def test_map_json_for_name_web(tmpdir): - data = map_json_for_name('iJO1366.Central metabolism', cache_dir=str(tmpdir)) - root = get_url('escher_root', protocol='https').rstrip('/') - assert json.loads(data)[0]['schema'] == '/'.join([root, 'escher', 'jsonschema', - __schema_version__ + '#']) # helper functions + def test_load_resource_json(tmpdir): test_json = '{"r": "val"}' assert _load_resource(test_json, 'name') == test_json + def test_load_resource_long_json(tmpdir): # this used to fail on Windows with Python 3 test_json = '{"r": "' + ('val' * 100000) + '"}' assert _load_resource(test_json, 'name') == test_json + def test_load_resource_directory(tmpdir): directory = os.path.abspath(os.path.dirname(__file__)) - assert _load_resource(join(directory, 'example.json'), 'name').strip() == '{"r": "val"}' + val = _load_resource(join(directory, 'example.json'), 'name').strip() + assert val == '{"r": "val"}' + def test_load_resource_invalid_file(tmpdir): with raises(ValueError) as err: @@ -125,62 +67,20 @@ def test_load_resource_invalid_file(tmpdir): _load_resource(p, 'name') assert 'not a valid json file' in err.value + @mark.web def test_load_resource_web(tmpdir): - url = '/'.join([get_url('map_download', protocol='https'), + url = '/'.join([get_url('map_download'), 'Escherichia%20coli/iJO1366.Central%20metabolism.json']) _ = json.loads(_load_resource(url, 'name')) -def test_Builder(tmpdir): - # ok with embedded_css arg - b = Builder(map_json='{"r": "val"}', model_json='{"r": "val"}', embedded_css='') - # b.display_in_notebook(js_source='local') - b.save_html(join(str(tmpdir), 'Builder.html'), js_source='local') - - # test options - with raises(Exception): - b._get_html(js_source='devv') - with raises(Exception): - b._get_html(menu='') - with raises(Exception): - b._get_html(scroll_behavior='asdf') - b._get_html(js_source='local') - b._get_html(menu='all') - b._get_html(scroll_behavior='zoom') - -@mark.web -def test_Builder_download(): - # download - b = Builder(map_name='iJO1366.Central metabolism', - model_name='iJO1366') - assert b.loaded_map_json is not None - assert b.loaded_model_json is not None - b._get_html(js_source='web') - # b.display_in_notebook(height=200) - - # data - b = Builder(map_name='iJO1366.Central metabolism', - model_name='iJO1366', - reaction_data=[{'GAPD': 123}, {'GAPD': 123}]) - b = Builder(map_name='iJO1366.Central metabolism', - model_name='iJO1366', - metabolite_data=[{'nadh_c': 123}, {'nadh_c': 123}]) - b = Builder(map_name='iJO1366.Central metabolism', - model_name='iJO1366', - gene_data=[{'gapA': 123}, {'adhE': 123}]) - - assert type(b.the_id) is unicode_type - assert len(b.the_id) == 10 - -def test_Builder_options(): - b = Builder(embedded_css='') - b.set_metabolite_no_data_color('white') - assert b.metabolite_no_data_color == 'white' - -def test__get_html(): +def test_save_html(tmpdir): + # ok with embedded_css arg b = Builder(map_json='"useless_map"', model_json='"useless_model"', embedded_css='') + filepath = join(str(tmpdir), 'builder.html') + b.save_html(filepath) def look_for_string(st, substring): """Look for the string in the substring. This solves a bug in py.test @@ -189,25 +89,28 @@ def look_for_string(st, substring): found = st.find(substring) assert found > -1 except AssertionError: - raise AssertionError('Could not find\n\n%s\n\nin\n\n%s' % (substring, st)) + raise AssertionError('Could not find\n\n%s\n\nin\n\n%s' % + (substring, st)) # no static parse, local - html = b._get_html(js_source='local') - look_for_string(html, 'map_data: JSON.parse(b64DecodeUnicode(\'InVzZWxlc3NfbWFwIg==\')),') - look_for_string(html, 'model_data: JSON.parse(b64DecodeUnicode(\'InVzZWxlc3NfbW9kZWwi\')),') - look_for_string(html, 'escher.Builder(t_map_data, t_model_data, data.builder_embed_css,') - - # static parse, not dev - static_index = '{"my": ["useless", "index"]}' + with open(filepath, 'r') as f: + html = f.read() + + look_for_string( + html, + 'map_data: JSON.parse(b64DecodeUnicode(\'InVzZWxlc3NfbWFwIg==\')),', + ) + look_for_string( + html, + 'model_data: JSON.parse(b64DecodeUnicode(\'InVzZWxlc3NfbW9kZWwi\')),', + ) + look_for_string( + html, + 'escher.Builder(data.map_data, data.model_data, ', + ) - html = b._get_html(static_site_index_json=static_index, protocol='https') - look_for_string(html, 'map_data: JSON.parse(b64DecodeUnicode(\'InVzZWxlc3NfbWFwIg==\')),') - look_for_string(html, 'model_data: JSON.parse(b64DecodeUnicode(\'InVzZWxlc3NfbW9kZWwi\')),') - map_download_url = base64.b64encode(("https://escher.github.io/%s/%s/maps/" % (__schema_version__, __map_model_version__)).encode('utf-8')).decode('utf-8') - look_for_string(html, 'map_download_url: b64DecodeUnicode(\'%s\'),' % map_download_url) - - model_download_url = base64.b64encode(("https://escher.github.io/%s/%s/models/" % (__schema_version__, __map_model_version__)).encode('utf-8')).decode('utf-8') - look_for_string(html, 'model_download_url: b64DecodeUnicode(\'%s\'),' % model_download_url) - look_for_string(html, 'eyJteSI6IFsidXNlbGVzcyIsICJpbmRleCJdfQ==') # static_index - look_for_string(html, 'escher.Builder(t_map_data, t_model_data, data.builder_embed_css,') +def test_Builder_options(): + b = Builder(embedded_css='') + b.set_metabolite_no_data_color('white') + assert b.metabolite_no_data_color == 'white' diff --git a/py/escher/tests/test_server.py b/py/escher/tests/test_server.py deleted file mode 100644 index 20695adf..00000000 --- a/py/escher/tests/test_server.py +++ /dev/null @@ -1,32 +0,0 @@ -from __future__ import print_function, unicode_literals - -import escher.server -import tornado.ioloop -from tornado.testing import AsyncHTTPTestCase, gen_test - -from pytest import mark - -class TestBuilder(AsyncHTTPTestCase): - def get_app(self): - return escher.server.application - - def test_index(self): - escher.server.PORT = self.get_http_port() - response = self.fetch('/') - assert response.code==200 - - @mark.web - def test_builder_request(self): - escher.server.PORT = self.get_http_port() - response = self.fetch('/builder/index.html') - assert response.code==200 - - def test_local_builder_request(self): - escher.server.PORT = self.get_http_port() - response = self.fetch('/builder/index.html?js_source=local') - assert response.code==200 - -# def test_server(): -# tornado.ioloop.IOLoop.instance().add_timeout(100, escher.server.stop) -# escher.server.run(port = 8123) -# print('stopped') diff --git a/py/escher/tests/test_urls.py b/py/escher/tests/test_urls.py index 480651a9..1fd5806a 100644 --- a/py/escher/tests/test_urls.py +++ b/py/escher/tests/test_urls.py @@ -1,37 +1,38 @@ -from escher.urls import get_url, names, root_directory -from escher.version import __version__, __schema_version__, __map_model_version__ -import os +from escher.urls import ( + get_url, + get_filepath, + root_directory, +) +from escher.version import ( + __version__, + __schema_version__, + __map_model_version__, +) from os.path import join, exists - from pytest import raises + def test_online(): - url = get_url('escher', source='web', protocol='https') + url = get_url('escher') assert url == 'https://unpkg.com/escher@%s/dist/escher.js' % __version__ -def test_no_protocol(): - url = get_url('escher', 'web') - assert url == '//unpkg.com/escher@%s/dist/escher.js' % __version__ def test_local(): - url = get_url('escher_min', 'local') - assert url == 'escher/static/escher/escher.min.js' - assert exists(join(root_directory, url)) + assert exists(get_filepath('map_jsonschema')) + + +def test_index_url(): + url = get_url('server_index') + assert url == ('https://escher.github.io/%s/%s/index.json' % + (__schema_version__, __map_model_version__)) -def test_localhost(): - url = get_url('escher', source='local', local_host='http://localhost:7778/') - assert url == 'http://localhost:7778/escher/static/escher/escher.js' -def test_download(): - url = get_url('server_index', source='local') - assert url == '../' + __schema_version__ + '/' + __map_model_version__ + '/index.json' - url = get_url('map_download', protocol='https') - assert url == 'https://escher.github.io/%s/%s/maps/' % (__schema_version__, __map_model_version__) +def test_map_download_url(): + url = get_url('map_download') + assert url == ('https://escher.github.io/%s/%s/maps/' % + (__schema_version__, __map_model_version__)) + def test_bad_url(): with raises(Exception): get_url('bad-name') - with raises(Exception): - get_url('d3', source='bad-source') - with raises(Exception): - get_url('d3', protocol='bad-protocol') diff --git a/py/escher/tests/test_utils.py b/py/escher/tests/test_utils.py index e0263b12..61694a7a 100644 --- a/py/escher/tests/test_utils.py +++ b/py/escher/tests/test_utils.py @@ -1,14 +1,16 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals +from __future__ import print_function, unicode_literals import base64 import json from escher.util import b64dump + def b64decode(str): return base64.b64decode(str.encode('utf-8')).decode('utf-8') + def test_b64dump(): assert b64decode(b64dump(None)) == 'null' accented_str = 'árvíztűrő tükörfúrógép' diff --git a/py/escher/urls.py b/py/escher/urls.py index bc655673..37a36c33 100644 --- a/py/escher/urls.py +++ b/py/escher/urls.py @@ -2,102 +2,61 @@ from __future__ import print_function, unicode_literals -from escher.version import __version__, __schema_version__, __map_model_version__ +from escher.version import ( + __version__, + __schema_version__, + __map_model_version__, +) import os import re from os.path import dirname, realpath, join root_directory = realpath(join(dirname(__file__), '..')) -top_directory = realpath(join(dirname(__file__), '..', '..')) +# relative to root_directory _escher_local = { - 'escher': 'escher/static/escher/escher.js', - 'escher_min': 'escher/static/escher/escher.min.js', - 'logo': 'escher/static/img/escher-logo@2x.png', - 'favicon': 'escher/static/img/favicon.ico', - 'homepage_js': 'escher/static/homepage/main.js', - 'homepage_css': 'escher/static/homepage/main.css', - 'server_index': '../%s/%s/index.json' % (__schema_version__, __map_model_version__), - 'map_download': '../%s/%s/maps/' % (__schema_version__, __map_model_version__), - 'model_download': '../%s/%s/models/' % (__schema_version__, __map_model_version__), + 'map_jsonschema': 'escher/static/jsonschema/1-0-0', } _escher_web = { - 'server_index': '%s/%s/index.json' % (__schema_version__, __map_model_version__), - 'map_download': '%s/%s/maps/' % (__schema_version__, __map_model_version__), - 'model_download': '%s/%s/models/' % (__schema_version__, __map_model_version__), - 'favicon': 'escher/static/img/favicon.ico', -} - -_dependencies = { + 'server_index': '%s/%s/index.json' % (__schema_version__, + __map_model_version__), + 'map_download': '%s/%s/maps/' % (__schema_version__, + __map_model_version__), + 'model_download': '%s/%s/models/' % (__schema_version__, + __map_model_version__), } _dependencies_cdn = { - 'escher': '//unpkg.com/escher@%s/dist/escher.js' % __version__, - 'escher_min': '//unpkg.com/escher@%s/dist/escher.min.js' % __version__, + 'escher': 'https://unpkg.com/escher@%s/dist/escher.js' % __version__, + 'escher_min': ('https://unpkg.com/escher@%s/dist/escher.min.js' % + __version__), } _links = { - 'escher_root': '//escher.github.io/', - 'github': '//github.com/zakandrewking/escher/', - 'github_releases': '//github.com/zakandrewking/escher/releases', - 'documentation': '//escher.readthedocs.org/', + 'escher_root': 'https://escher.github.io/', + 'github': 'https://github.com/zakandrewking/escher', + 'github_releases': 'https://github.com/zakandrewking/escher/releases', + 'documentation': 'https://escher.readthedocs.org/', } -# external dependencies -names = list(_escher_local.keys()) + list(_escher_web.keys()) + list(_dependencies.keys()) + list(_links.keys()) - -def get_url(name, source='web', local_host=None, protocol=None): - """Get a url. - - Arguments - --------- - - name: The name of the URL. Options are available in urls.names. - - source: Either 'web' or 'local'. Cannot be 'local' for external links. - - protocol: The protocol can be 'http', 'https', or None which indicates a - 'protocol relative URL', as in //escher.github.io. Ignored if source is - local. - local_host: A host url, including the protocol. e.g. http://localhost:7778. - - """ - if source not in ['web', 'local']: - raise Exception('Bad source: %s' % source) - - if protocol not in [None, 'http', 'https']: - raise Exception('Bad protocol: %s' % protocol) - - if protocol is None: - protocol = '' +def get_filepath(key): + """Get the filepath for the key""" + if key in _escher_local: + return join(root_directory, _escher_local[key]) else: - protocol = protocol + ':' + raise Exception('File key not recognized: %s' % key) - def apply_local_host(url): - return '/'.join([local_host.rstrip('/'), url.lstrip('/')]) - # escher - if name in _escher_local and source == 'local': - if local_host is not None: - return apply_local_host(_escher_local[name]) - return _escher_local[name] - elif name in _escher_web and source == 'web': - return protocol + '/'.join([_links['escher_root'].rstrip('/'), - _escher_web[name].lstrip('/')]) - # links - elif name in _links: - if source=='local': - raise Exception('Source cannot be "local" for external links') - return protocol + _links[name] - # local dependencies - elif name in _dependencies and source == 'local': - if local_host is not None: - return apply_local_host(_dependencies[name]) - return _dependencies[name] - # cdn dependencies - elif name in _dependencies_cdn and source == 'web': - return protocol + _dependencies_cdn[name] +def get_url(name): + """Get a url for the key""" - raise Exception('name not found') + if name in _escher_web: + return _links['escher_root'] + _escher_web[name] + elif name in _links: + return _links[name] + elif name in _dependencies_cdn: + return _dependencies_cdn[name] + else: + raise Exception('name not found') diff --git a/py/escher/util.py b/py/escher/util.py index 52332ac5..8db8d89e 100644 --- a/py/escher/util.py +++ b/py/escher/util.py @@ -1,40 +1,9 @@ +# -*- coding: utf-8 -*- +from __future__ import print_function, unicode_literals + import base64 import json -import sys - -# user input for python 2 and 3 -try: - import __builtin__ - input = getattr(__builtin__, 'raw_input') -except (ImportError, AttributeError): - pass - -def query_yes_no(question): - """Ask a yes/no question via input() and return their answer. - - Returns True for yes or False for no. - - Arguments - --------- - question: A string that is presented to the user. - - - Adapted from http://stackoverflow.com/questions/3041986/python-command-line-yes-no-input. - - """ - valid = {"yes": True, "y": True, "ye": True, - "no": False, "n": False} - prompt = " [y/n] " - - while True: - sys.stdout.write(question + prompt) - choice = input().lower() - try: - return valid[choice] - except KeyError: - sys.stdout.write("Please respond with 'yes' or 'no' " - "(or 'y' or 'n').\n") def b64dump(data): """Returns the base64 encoded dump of the input @@ -43,6 +12,7 @@ def b64dump(data): --------- data: Can be a dict, a (JSON or plain) string, or None + """ if isinstance(data, dict): data = json.dumps(data) diff --git a/py/escher/validate.py b/py/escher/validate.py index 9691209a..bf62d19d 100644 --- a/py/escher/validate.py +++ b/py/escher/validate.py @@ -3,7 +3,7 @@ from __future__ import print_function, unicode_literals from escher import __schema_version__ -from escher.urls import root_directory +from escher.urls import get_filepath from os.path import join import re import json @@ -20,6 +20,7 @@ """ + def main(): if len(sys.argv) < 2: print(usage_string) @@ -30,6 +31,7 @@ def main(): validate_map(map_data) print('Your map passed inspection and is free of infection.') + def validate_map(map_data): """Validate a map using the jsonschema, and some extra checks for consistency.""" import jsonschema @@ -56,11 +58,13 @@ def validate_map(map_data): if error != '': raise Exception(error) + def validate_schema(): import jsonschema schema = get_jsonschema() jsonschema.Draft4Validator.check_schema(schema) + def check_map(map_data): """Check reactions and metabolites. @@ -98,13 +102,15 @@ def check_map(map_data): missing_gene_names.append(found_gene) return bad_segments, missing_multimarkers, missing_stoich, missing_gene_names + def get_jsonschema(): """Get the local jsonschema. """ - with open(join(root_directory, 'escher', 'static', 'jsonschema', __schema_version__), 'r') as f: + with open(get_filepath('map_jsonschema'), 'r') as f: return json.load(f) + def genes_for_gene_reaction_rule(rule): """ Find genes in gene_reaction_rule string. diff --git a/py/escher/version.py b/py/escher/version.py index fec4ed4c..63633e27 100644 --- a/py/escher/version.py +++ b/py/escher/version.py @@ -16,7 +16,7 @@ def get_full_version(main_version, post_version=None): # to get autodoc going package = defaultdict(str) else: - with open(join(dirname(__file__), 'package.json'), 'r') as f: + with open(join(dirname(__file__), 'static', 'package.json'), 'r') as f: package = json.load(f) # software version diff --git a/py/jupyter-escher.json b/py/jupyter-escher.json new file mode 100644 index 00000000..76a26bff --- /dev/null +++ b/py/jupyter-escher.json @@ -0,0 +1,5 @@ +{ + "load_extensions": { + "jupyter-escher/extension": true + } +} diff --git a/py/setup.py b/py/setup.py index 4396f3c1..3ef61e96 100644 --- a/py/setup.py +++ b/py/setup.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from __future__ import print_function, unicode_literals import sys from sys import argv @@ -11,13 +12,7 @@ from glob import glob import re -try: - from setuptools import setup, Command - from setuptools.command.sdist import sdist as SDistCommand - from setuptools.command.bdist import bdist as BDistCommand - from setuptools.command.upload import upload as UploadCommand -except ImportError: - from distutils.core import setup, Command +from setuptools import setup, find_packages, Command directory = dirname(realpath(__file__)) sys.path.insert(0, join(directory, 'escher')) @@ -26,59 +21,6 @@ package = __import__('version').package port = 8789 -class CleanCommand(Command): - description = "Custom clean command that removes static site" - user_options = [] - def initialize_options(self): - pass - def finalize_options(self): - pass - def run(self): - def remove_if(x): - if exists(x): rmtree(x) - remove_if(join(directory, 'build')) - remove_if(join(directory, 'dist')) - # remove site files - remove_if(join(directory, '..', 'builder')) - for f in glob(join(directory, '..', 'index.html')): - os.remove(f) - print('done cleaning') - - -class BuildGHPagesCommand(Command): - description = "Custom build command that generates static site, and copies escher libs" - user_options = [] - def initialize_options(self): - pass - def finalize_options(self): - pass - def run(self): - # generate the static site - try: - from escher import generate_index, static_site - except ImportError: - raise Exception('Escher not installed') - generate_index.main() - static_site.generate_static_site() - print('Done building gh-pages') - - -class TestCommand(Command): - description = "Custom test command that runs pytest" - user_options = [('noweb', None, 'Skip run tests that require the Escher website')] - def initialize_options(self): - self.noweb = False - def finalize_options(self): - pass - def run(self): - import pytest - if self.noweb: - exit_code = pytest.main(['-m', 'not web']) - else: - exit_code = pytest.main([]) - sys.exit(exit_code) - - setup( name='Escher', version=full_version, @@ -97,26 +39,35 @@ def run(self): 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', 'Operating System :: OS Independent' ], - packages=['escher'], - package_data={'escher': ['package.json', 'static/escher/*', 'static/fonts/*', - 'static/jsonschema/*', 'static/homepage/*', - 'static/img/*', 'static/lib/*', 'templates/*']}, - install_requires=['Jinja2>=2.7.3', - 'tornado>=4.0.2', - 'pytest>=2.6.2', - 'cobra>=0.3.0', - 'jsonschema>=2.4.0'], - extras_require={'docs': ['sphinx>=1.2', - 'sphinx-rtd-theme>=0.1.6'], - 'all': ['sphinx>=1.2', - 'sphinx-rtd-theme>=0.1.6', - 'ipython>=4.0.2', - 'jupyter>=1.0.0', - 'wheel>=0.24.0', - 'twine>=1.5.0'] }, - cmdclass={'clean': CleanCommand, - 'build_gh': BuildGHPagesCommand, - 'test': TestCommand} + packages=find_packages(), + include_package_data=True, + data_files=[ + ( + 'share/jupyter/nbextensions/jupyter-escher', + [ + 'escher/static/extension.js', + 'escher/static/escher.min.js', + 'escher/static/escher.min.js.map', + ] + ), + ( + 'etc/jupyter/nbconfig/notebook.d', + ['jupyter-escher.json'], + ) + ], + install_requires=[ + 'Jinja2>=2.7.3,<3', + 'tornado>=4.0.2,<5', + 'pytest>=4.0.1,<5', + 'cobra>=0.5.0', + 'jsonschema>=2.4.0,<3', + 'ipywidgets>=7.1.0,<8', + ], + extras_require={ + 'docs': ['sphinx>=1.2', 'sphinx-rtd-theme>=0.1.6'], + }, ) diff --git a/src/Behavior.js b/src/Behavior.js index d3504125..784e7d22 100644 --- a/src/Behavior.js +++ b/src/Behavior.js @@ -1,1040 +1,990 @@ -/** - * 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.label_touch, - * my_behavior.bezier_drag, - * my_behavior.bezier_mouseover, my_behavior.bezier_mouseout, - * my_behavior.reaction_label_drag, my_behavior.node_label_drag, - * my_behavior.object_mouseover, my_behavior.object_touch, - * my_behavior.object_mouseout - * - */ +import utils from './utils' +import build from './build' +import { drag as d3Drag } from 'd3-drag' +import * as d3Selection from 'd3-selection' -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_label_touch: toggle_label_touch, - toggle_object_mouseover: toggle_object_mouseover, - toggle_object_touch: toggle_object_touch, - 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.label_touch = null - this.object_mouseover = null - this.object_touch = null - this.object_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() -} +const d3Select = d3Selection.select +const d3Mouse = d3Selection.mouse /** - * 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) - this.toggle_label_touch(true) - this.toggle_object_mouseover(true) - this.toggle_object_touch(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) - this.toggle_label_touch(false) - this.toggle_object_mouseover(false) - this.toggle_object_touch(false) -} - -/** - * Listen for rotation, and rotate selected nodes. + * Behavior. Defines the set of click and drag behaviors for the map, and keeps + * track of which behaviors are activated. + * @param map {Map} - + * @param undoStack {UndoStack} - */ -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) +export default class Behavior { + constructor (map, undoStack) { + this.map = map + this.undoStack = undoStack + + // make an empty function that can be called as a behavior and does nothing + this.emptyBehavior = () => {} + + // rotation mode operates separately from the rest + this.rotationModeEnabled = false + this.rotationDrag = d3Drag() + + // behaviors to be applied + this.selectableMousedown = null + this.textLabelMousedown = null + this.textLabelClick = null + this.selectableDrag = this.emptyBehavior + this.nodeMouseover = null + this.nodeMouseout = null + this.labelMousedown = null + this.labelMouseover = this.emptyBehavior + this.labelMouseout = null + this.labelTouch = null + this.objectMouseover = this.emptyBehavior + this.objectTouch = null + this.objectMouseout = null + this.bezierDrag = this.emptyBehavior + this.bezierMouseover = null + this.bezierMouseout = null + this.reactionLabelDrag = this.emptyBehavior + this.nodeLabelDrag = this.emptyBehavior + this.dragging = false + this.turnEverythingOn() + } + + /** + * Toggle everything except rotation mode and text mode. + */ + turnEverythingOn () { + this.toggleSelectableClick(true) + this.toggleSelectableDrag(true) + this.toggleLabelDrag(true) + this.toggleLabelMouseover(true) + this.toggleLabelTouch(true) + this.toggleObjectMouseover(true) + this.toggleObjectTouch(true) + } + + /** + * Toggle everything except rotation mode and text mode. + */ + turnEverythingOff () { + this.toggleSelectableClick(false) + this.toggleSelectableDrag(false) + this.toggleLabelDrag(false) + this.toggleLabelMouseover(false) + this.toggleLabelTouch(false) + this.toggleObjectMouseover(false) + this.toggleObjectTouch(false) + } + + averageLocation (nodes) { + const xs = [] + const ys = [] + for (const nodeId in nodes) { + const node = nodes[nodeId] + if (node.x !== undefined) xs.push(node.x) + if (node.y !== undefined) ys.push(node.y) } - 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) + return { + x: utils.mean(xs), + y: utils.mean(ys) } - 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') + showCenter () { + const sel = this.map.sel.selectAll('#rotation-center').data([ 0 ]) + const enterSel = sel.enter().append('g').attr('id', 'rotation-center') - enter_sel.append('path').attr('d', 'M-32 0 L32 0') + enterSel.append('path').attr('d', 'M-32 0 L32 0') .attr('class', 'rotation-center-line') - enter_sel.append('path').attr('d', 'M0 -32 L0 32') + enterSel.append('path').attr('d', 'M0 -32 L0 32') .attr('class', 'rotation-center-line') - var update_sel = enter_sel.merge(sel) + const updateSel = enterSel.merge(sel) - update_sel.attr('transform', - 'translate(' + this.center.x + ',' + this.center.y + ')') + updateSel.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') + const current = parseFloat(updateSel.selectAll('path').style('stroke-width')) + updateSel.selectAll('path').style('stroke-width', current * 2 + 'px') }) .on('mouseout', function () { - update_sel.selectAll('path').style('stroke-width', null) + updateSel.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] + .call(d3Drag().on('drag', () => { + const cur = utils.d3_transform_catch(updateSel.attr('transform')) + const newLoc = [ + d3Selection.event.dx + cur.translate[0], + d3Selection.event.dy + cur.translate[1] ] - update_sel.attr('transform', 'translate(' + new_loc + ')') - this.center = { x: new_loc[0], y: new_loc[1] } - }.bind(this))) + updateSel.attr('transform', 'translate(' + newLoc + ')') + this.center = { x: newLoc[0], y: newLoc[1] } + })) } - function hide_center(sel) { + + hideCenter () { 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) + /** + * Listen for rotation, and rotate selected nodes. + */ + toggleRotationMode (onOff) { + if (onOff === undefined) { + this.rotationModeEnabled = !this.rotationModeEnabled + } else { + this.rotationModeEnabled = onOff } - } 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') - } -} + const selectionNode = this.map.sel.selectAll('.node-circle') + const selectionBackground = this.map.sel.selectAll('#canvas') -/** - * 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 - } -} + if (this.rotationModeEnabled) { + this.map.callback_manager.run('start_rotation') -/** - * 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 - } -} + const selectedNodes = this.map.getSelectedNodes() + if (Object.keys(selectedNodes).length === 0) { + console.warn('No selected nodes') + return + } -/** - * 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 - } + // show center + this.center = this.averageLocation(selectedNodes) + this.showCenter() - if (on_off) { + // this.setStatus('Drag to rotate.') + const map = this.map + const selectedNodeIds = Object.keys(selectedNodes) + const reactions = this.map.reactions + const nodes = this.map.nodes + const beziers = this.map.beziers - // 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) + const startFn = d => { + // silence other listeners + d3Selection.event.sourceEvent.stopPropagation() } - }.bind(this) - - this.label_mouseout = function () { - this.map.callback_manager.run('delay_hide_tooltip') - }.bind(this) - - } else { - this.label_mouseover = null + const dragFn = (d, angle, totalAngle, center) => { + const updated = build.rotate_nodes(selectedNodes, reactions, + beziers, angle, center) + map.draw_these_nodes(updated.node_ids) + map.draw_these_reactions(updated.reaction_ids) + } + const endFn = d => {} + const undoFn = (d, totalAngle, center) => { + // undo + const theseNodes = {} + selectedNodeIds.forEach(function (id) { + theseNodes[id] = nodes[id] + }) + const updated = build.rotate_nodes(theseNodes, reactions, + beziers, -totalAngle, + center) + map.draw_these_nodes(updated.node_ids) + map.draw_these_reactions(updated.reaction_ids) + } + const redoFn = (d, totalAngle, center) => { + // redo + const theseNodes = {} + selectedNodeIds.forEach(id => { + theseNodes[id] = nodes[id] + }) + const updated = build.rotate_nodes(theseNodes, reactions, + beziers, totalAngle, + center) + map.draw_these_nodes(updated.node_ids) + map.draw_these_reactions(updated.reaction_ids) + } + const centerFn = () => this.center + this.rotationDrag = this.getGenericAngularDrag(startFn, dragFn, + endFn, undoFn, + redoFn, centerFn, + this.map.sel) + selectionBackground.call(this.rotationDrag) + this.selectableDrag = this.rotationDrag + } else { + // turn off all listeners + this.hideCenter() + selectionNode.on('mousedown.center', null) + selectionBackground.on('mousedown.center', null) + selectionBackground.on('mousedown.drag', null) + selectionBackground.on('touchstart.drag', null) + this.rotationDrag = null + this.selectableDrag = null + } } -} -/** - * With no argument, toggle the tooltips upon touching of labels. - * @param {Boolean} on_off - The new on/off state. If this argument is not provided, then toggle the state. - */ -function toggle_label_touch (on_off) { - if (on_off === undefined) { - on_off = this.label_touch === null + /** + * With no argument, toggle the node click on or off. Pass in a boolean argument + * to set the on/off state. + */ + toggleSelectableClick (onOff) { + if (onOff === undefined) { + onOff = this.selectableMousedown === null + } + if (onOff) { + const map = this.map + this.selectableMousedown = d => { + // stop propogation for the buildinput to work right + d3Selection.event.stopPropagation() + // this.parentNode.__data__.wasSelected = d3Select(this.parentNode).classed('selected') + // d3Select(this.parentNode).classed('selected', true) + } + this.selectableClick = function (d) { + // stop propogation for the buildinput to work right + d3Selection.event.stopPropagation() + // click suppressed. This DOES have en effect. + if (d3Selection.event.defaultPrevented) return + // turn off the temporary selection so select_selectable + // works. This is a bit of a hack. + // if (!this.parentNode.__data__.wasSelected) + // d3Select(this.parentNode).classed('selected', false) + map.select_selectable(this, d, d3Selection.event.shiftKey) + // this.parentNode.__data__.wasSelected = false + } + this.nodeMouseover = function (d) { + d3Select(this).style('stroke-width', null) + const current = parseFloat(d3Select(this).style('stroke-width')) + if (!d3Select(this.parentNode).classed('selected')) { + d3Select(this).style('stroke-width', current * 3 + 'px') + } + } + this.nodeMouseout = function (d) { + d3Select(this).style('stroke-width', null) + } + } else { + this.selectableMousedown = null + this.selectableClick = null + this.nodeMouseover = null + this.nodeMouseout = null + this.map.sel.select('#nodes') + .selectAll('.node-circle').style('stroke-width', 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_touch = (type, d) => { - if (!this.dragging) { - this.map.callback_manager.run('show_tooltip', null, type, d) + /** + * 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 + * selectableMousedown. + */ + toggleTextLabelEdit (onOff) { + if (onOff === undefined) { + onOff = this.textEditMousedown == null + } + if (onOff) { + const map = this.map + this.textLabelMousedown = function () { + if (d3Selection.event.defaultPrevented) { + return // mousedown suppressed + } + // run the callback + const coordsA = utils.d3_transform_catch(d3Select(this).attr('transform')).translate + const coords = { x: coordsA[0], y: coordsA[1] } + map.callback_manager.run('edit_text_label', null, d3Select(this), coords) + d3Selection.event.stopPropagation() } + this.textLabelClick = 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 + d3Selection.event.preventDefault() + const coords = { + x: d3Mouse(node)[0], + y: d3Mouse(node)[1] + } + this.map.callback_manager.run('new_text_label', null, coords) + }.bind(this, this.map.sel.node())) + } else { + this.textLabelMousedown = this.selectableMousedown + this.textLabelClick = this.selectableClick + 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') } - } else { - this.label_touch = null } -} -/** - * With no argument, toggle the tooltips on mouseover of nodes or arrows. - * @param {Boolean} on_off - The new on/off state. - */ -function toggle_object_mouseover (on_off) { - if (on_off === undefined) { - on_off = this.label_mouseover === null + /** + * With no argument, toggle the node drag & bezier drag on or off. Pass in a + * boolean argument to set the on/off state. + */ + toggleSelectableDrag (onOff) { + if (onOff === undefined) { + onOff = this.selectableDrag === this.emptyBehavior + } + if (onOff) { + this.selectableDrag = this.getSelectableDrag(this.map, this.undoStack) + this.bezierDrag = this.getBezierDrag(this.map, this.undoStack) + } else { + this.selectableDrag = this.emptyBehavior + this.bezierDrag = this.emptyBehavior + } } - if (on_off) { + /** + * With no argument, toggle the label drag on or off. Pass in a boolean argument + * to set the on/off state. + * @param {Boolean} onOff - The new on/off state. + */ + toggleLabelDrag (onOff) { + if (onOff === undefined) { + onOff = this.labelDrag === this.emptyBehavior + } + if (onOff) { + this.reactionLabelDrag = this.getReactionLabelDrag(this.map) + this.nodeLabelDrag = this.getNodeLabelDrag(this.map) + } else { + this.reactionLabelDrag = this.emptyBehavior + this.nodeLabelDrag = this.emptyBehavior + } + } - // Show/hide tooltip. - // @param {String} type - 'reaction_object' or 'node_object' - // @param {Object} d - D3 data for DOM element - this.object_mouseover = function (type, d) { - if (!this.dragging) { - this.map.callback_manager.run('show_tooltip', null, type, d) - } - }.bind(this) + /** + * With no argument, toggle the tooltips on mouseover labels. + * @param {Boolean} onOff - The new on/off state. + */ + toggleLabelMouseover (onOff) { + if (onOff === undefined) { + onOff = this.labelMouseover === this.emptyBehavior + } - this.object_mouseout = function () { - this.map.callback_manager.run('delay_hide_tooltip') - }.bind(this) + if (onOff) { + // Show/hide tooltip. + // @param {String} type - 'reactionLabel' or 'nodeLabel' + // @param {Object} d - D3 data for DOM element + this.labelMouseover = (type, d) => { + if (!this.dragging) { + this.map.callback_manager.run('show_tooltip', null, type, d) + } + } - } else { - this.object_mouseover = null + this.labelMouseout = () => { + this.map.callback_manager.run('delay_hide_tooltip') + } + } else { + this.labelMouseover = this.emptyBehavior + } } -} -/** - * With no argument, toggle the tooltips upon touching of nodes or arrows. - * @param {Boolean} on_off - The new on/off state. If this argument is not provided, then toggle the state. - */ -function toggle_object_touch (on_off) { - if (on_off === undefined) { - on_off = this.label_touch === null - } + /** + * With no argument, toggle the tooltips upon touching of labels. + * @param {Boolean} onOff - The new on/off state. If this argument is not + * provided, then toggle the state. + */ + toggleLabelTouch (onOff) { + if (onOff === undefined) { + onOff = this.labelTouch === null + } - if (on_off) { - this.object_touch = (type, d) => { - if (!this.dragging) { - this.map.callback_manager.run('show_tooltip', null, type, d) + if (onOff) { + // Show/hide tooltip. + // @param {String} type - 'reactionLabel' or 'nodeLabel' + // @param {Object} d - D3 data for DOM element + this.labelTouch = (type, d) => { + if (!this.dragging) { + this.map.callback_manager.run('show_tooltip', null, type, d) + } } + } else { + this.labelTouch = null } - } else { - this.object_touch = 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') + /** + * With no argument, toggle the tooltips on mouseover of nodes or arrows. + * @param {Boolean} onOff - The new on/off state. + */ + toggleObjectMouseover (onOff) { + if (onOff === undefined) { + onOff = this.objectMouseover === this.emptyBehavior } - } 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) -} + if (onOff) { + // Show/hide tooltip. + // @param {String} type - 'reaction_object' or 'node_object' + // @param {Object} d - D3 data for DOM element + this.objectMouseover = (type, d) => { + if (!this.dragging) { + this.map.callback_manager.run('show_tooltip', null, type, d) + } + } -/** - * 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) - } - }) + this.objectMouseout = () => { + this.map.callback_manager.run('delay_hide_tooltip') + } + } else { + this.objectMouseover = this.emptyBehavior } - }) + } - 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) + /** + * With no argument, toggle the tooltips upon touching of nodes or arrows. + * @param {Boolean} onOff - The new on/off state. If this argument is not provided, then toggle the state. + */ + toggleObjectTouch (onOff){ + if (onOff === undefined) { + onOff = this.labelTouch === null } - // 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 + if (onOff) { + this.objectTouch = (type, d) => { + if (!this.dragging) { + this.map.callback_manager.run('show_tooltip', null, type, d) + } + } } else { - // if it is a text label - grabbed['type'] = 'label' - grabbed['id'] = this.__data__.text_label_id + this.objectTouch = null } + } - 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 + /** + * With no argument, toggle the bezier drag on or off. Pass in a boolean + * argument to set the on/off state. + */ + toggleBezierDrag (onOff) { + if (onOff === undefined) { + onOff = this.bezierDrag === this.emptyBehavior } - // 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) - }) - + if (onOff) { + this.bezierDrag = this.getBezierDrag(this.map) + this.bezierMouseover = function (d) { + d3Select(this).style('stroke-width', String(3)+'px') + } + this.bezierMouseout = function (d) { + d3Select(this).style('stroke-width', String(1)+'px') + } } 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) - }) + this.bezierDrag = this.emptyBehavior + this.bezierMouseover = null + this.bezierMouseout = null } + } - // 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) { + turnOffDrag (sel) { + sel.on('mousedown.drag', null) + sel.on('touchstart.drag', null) + } + + combineNodesAndDraw (fixedNodeId, draggedNodeId) { + const map = this.map + const draggedNode = map.nodes[draggedNodeId] + const fixedNode = map.nodes[fixedNodeId] + const updatedSegmentObjs = [] + draggedNode.connectedSegments.forEach(segmentObj => { // change the segments to reflect - var segment + let segment = null try { - segment = map.reactions[segment_obj.reaction_id].segments[segment_obj.segment_id] + segment = map.reactions[segmentObj.reactionId].segments[segmentObj.segmentId] if (segment === undefined) throw new Error('undefined segment') } catch (e) { - console.warn('Could not find connected segment ' + segment_obj.segment_id) + console.warn('Could not find connected segment ' + segmentObj.segmentId) 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 + if (segment.fromNodeId==draggedNodeId) segment.fromNodeId = fixedNodeId + else if (segment.toNodeId==draggedNodeId) segment.toNodeId = fixedNodeId 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)) + // moved segmentObj to fixedNode + fixedNode.connectedSegments.push(segmentObj) + updatedSegmentObjs.push(utils.clone(segmentObj)) return }) // delete the old node - map.delete_node_data([dragged_node_id]) + map.delete_node_data([draggedNodeId]) // turn off the class map.sel.selectAll('.node-to-combine').classed('node-to-combine', false) // draw - map.draw_everything() + map.drawEverything() // return for undo - return updated_segment_objs - } -} + return updatedSegmentObjs + } + + /** + * Drag the selected nodes and text labels. + * @param {} map - + * @param {} undo_stack - + */ + getSelectableDrag (map, undoStack) { + // define some variables + const behavior = d3Drag() + let theTimeout = null + let totalDisplacement = null + // for nodes + let nodeIdsToDrag = null + let reactionIds = null + // for text labels + let textLabelIdsToDrag = null + const moveLabel = (textLabelId, displacement) => { + const textLabel = map.text_labels[textLabelId] + textLabel.x = textLabel.x + displacement.x + textLabel.y = textLabel.y + displacement.y + } + const setDragging = onOff => { + this.dragging = onOff + } -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) -} + behavior.on('start', function (d) { + setDragging(true) + + // silence other listeners (e.g. nodes BELOW this one) + d3Selection.event.sourceEvent.stopPropagation() + // remember the total displacement for later + totalDisplacement = { x: 0, y: 0 } + + // If a text label is selected, the rest is not necessary + if (d3Select(this).attr('class').indexOf('label') === -1) { + // Note that drag start is called even for a click event + const data = this.parentNode.__data__ + const biggId = data.biggId + const nodeGroup = 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. + theTimeout = setTimeout(() => { + nodeGroup.parentNode.insertBefore(nodeGroup, nodeGroup.parentNode.firstChild) + }, 200) + // prepare to combine metabolites + map.sel.selectAll('.metabolite-circle') + .on('mouseover.combine', function (d) { + if (d.bigg_id === biggId && d.node_id !== data.nodeId) { + d3Select(this).style('stroke-width', String(12) + 'px') + .classed('node-to-combine', true) + } + }) + .on('mouseout.combine', d => { + if (d.bigg_id === biggId) { + map.sel.selectAll('.node-to-combine') + .style('stroke-width', String(2) + 'px') + .classed('node-to-combine', false) + } + }) + } + }) -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) -} + 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 (!d3Select(this.parentNode).classed('selected')) { + map.select_selectable(this, d) + } -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) { + // get the grabbed id + const grabbed = {} + if (d3Select(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 + } + + const selectedNodeIds = map.get_selected_node_ids() + const selectedTextLabelIds = map.get_selected_text_label_ids() + nodeIdsToDrag = [] + textLabelIdsToDrag = [] + // choose the nodes and text labels to drag + if (grabbed['type'] === 'node' && + selectedNodeIds.indexOf(grabbed['id']) === -1) { + nodeIdsToDrag.push(grabbed['id']) + } else if (grabbed['type'] === 'label' && + selectedTextLabelIds.indexOf(grabbed['id']) === -1) { + textLabelIdsToDrag.push(grabbed['id']) + } else { + nodeIdsToDrag = selectedNodeIds + textLabelIdsToDrag = selectedTextLabelIds + } + reactionIds = [] + const displacement = { + x: d3Selection.event.dx, + y: d3Selection.event.dy + } + totalDisplacement = utils.c_plus_c(totalDisplacement, displacement) + nodeIdsToDrag.forEach(nodeId => { + // update data + const node = map.nodes[nodeId] + const updated = build.move_node_and_dependents(node, nodeId, + map.reactions, + map.beziers, + displacement) + reactionIds = utils.uniqueConcat([ reactionIds, updated.reaction_ids ]) + // remember the displacements + // if (!(nodeId in totalDisplacement)) totalDisplacement[nodeId] = { x: 0, y: 0 } + // totalDisplacement[nodeId] = utils.c_plus_c(totalDisplacement[nodeId], displacement) + }) + textLabelIdsToDrag.forEach(textLabelId => { + moveLabel(textLabelId, displacement) + // remember the displacements + // if (!(nodeId in totalDisplacement)) totalDisplacement[nodeId] = { x: 0, y: 0 } + // totalDisplacement[nodeId] = utils.c_plus_c(totalDisplacement[nodeId], displacement) + }) + // draw + map.draw_these_nodes(nodeIdsToDrag) + map.draw_these_reactions(reactionIds) + map.draw_these_text_labels(textLabelIdsToDrag) + }) + + behavior.on('end', () => { + setDragging(false) + + if (nodeIdsToDrag === null) { + // Drag end can be called when drag has not been called. In this, case, do + // nothing. + totalDisplacement = null + nodeIdsToDrag = null + textLabelIdsToDrag = null + reactionIds = null + theTimeout = null + return + } + + // look for mets to combine + const nodeToCombineArray = [] + map.sel.selectAll('.node-to-combine').each(d => { + nodeToCombineArray.push(d.node_id) + }) + + if (nodeToCombineArray.length === 1) { + // If a node is ready for it, combine nodes + const fixedNodeId = nodeToCombineArray[0] + const draggedNodeId = this.parentNode.__data__.node_id + const savedDraggedNode = utils.clone(map.nodes[draggedNodeId]) + const segmentObjsMovedToCombine = this.combineNodesAndDraw(fixedNodeId, + draggedNodeId) + undoStack.push(() => { + // undo + // put the old node back + map.nodes[draggedNodeId] = savedDraggedNode + const fixedNode = map.nodes[fixedNodeId] + const updatedReactions = [] + segmentObjsMovedToCombine.forEach(segmentObj => { + const segment = map.reactions[segmentObj.reactionId].segments[segmentObj.segmentId] + if (segment.fromNodeId === fixedNodeId) { + segment.fromNodeId = draggedNodeId + } else if (segment.toNodeId === fixedNodeId) { + segment.toNodeId = draggedNodeId + } else { + console.error('Segment does not connect to fixed node') + } + // removed this segmentObj from the fixed node + fixedNode.connectedSegments = fixedNode.connectedSegments.filter(x => { + return !(x.reactionId === segmentObj.reactionId && x.segmentId === segmentObj.segmentId) + }) + if (updatedReactions.indexOf(segmentObj.reactionId) === -1) { + updatedReactions.push(segmentObj.reactionId) + } + }) + map.draw_these_nodes([draggedNodeId]) + map.draw_these_reactions(updatedReactions) + }, () => { + // redo + this.combineNodesAndDraw(fixedNodeId, draggedNodeId) + }) + } else { + // otherwise, drag node + + // add to undo/redo stack + // remember the displacement, dragged nodes, and reactions + const savedDisplacement = utils.clone(totalDisplacement) + // BUG TODO this variable disappears! + // Happens sometimes when you drag a node, then delete it, then undo twice + const savedNodeIds = utils.clone(nodeIdsToDrag) + const savedTextLabelIds = utils.clone(textLabelIdsToDrag) + const savedReactionIds = utils.clone(reactionIds) + undoStack.push(() => { + // undo + savedNodeIds.forEach(nodeId => { + const node = map.nodes[nodeId] + build.move_node_and_dependents(node, nodeId, map.reactions, + map.beziers, + utils.c_times_scalar(savedDisplacement, -1)) + }) + savedTextLabelIds.forEach(textLabelId => { + moveLabel(textLabelId, + utils.c_times_scalar(savedDisplacement, -1)) + }) + map.draw_these_nodes(savedNodeIds) + map.draw_these_reactions(savedReactionIds) + map.draw_these_text_labels(savedTextLabelIds) + }, () => { + // redo + savedNodeIds.forEach(nodeId => { + const node = map.nodes[nodeId] + build.move_node_and_dependents(node, nodeId, map.reactions, + map.beziers, + savedDisplacement) + }) + savedTextLabelIds.forEach(textLabelId => { + moveLabel(textLabelId, savedDisplacement) + }) + map.draw_these_nodes(savedNodeIds) + map.draw_these_reactions(savedReactionIds) + map.draw_these_text_labels(savedTextLabelIds) + }) + } + + // stop combining metabolites + map.sel.selectAll('.metabolite-circle') + .on('mouseover.combine', null) + .on('mouseout.combine', null) + + // clear the timeout + clearTimeout(theTimeout) + + // clear the shared variables + totalDisplacement = null + nodeIdsToDrag = null + textLabelIdsToDrag = null + reactionIds = null + theTimeout = null + }) + + return behavior } - var undo_fn = function (d, displacement) { - move_label(d.node_id, utils.c_times_scalar(displacement, -1)) - map.draw_these_nodes ([ d.node_id ]) + + getBezierDrag (map) { + const moveBezier = (reactionId, segmentId, bez, bezierId, displacement) => { + const segment = map.reactions[reactionId].segments[segmentId] + segment[bez] = utils.c_plus_c(segment[bez], displacement) + map.beziers[bezierId].x = segment[bez].x + map.beziers[bezierId].y = segment[bez].y + } + const startFn = d => { + d.dragging = true + } + const dragFn = (d, displacement, totalDisplacement) => { + // draw + moveBezier(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]) + } + const endFn = d => { + d.dragging = false + } + const undoFn = (d, displacement) => { + moveBezier(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]) + } + const redoFn = (d, displacement) => { + moveBezier(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.getGenericDrag(startFn, dragFn, endFn, undoFn, redoFn, + this.map.sel) } - var redo_fn = function (d, displacement) { - move_label(d.node_id, displacement) - map.draw_these_nodes([ d.node_id ]) + + getReactionLabelDrag (map) { + const moveLabel = (reactionId, displacement) => { + const reaction = map.reactions[reactionId] + reaction.label_x = reaction.label_x + displacement.x + reaction.label_y = reaction.label_y + displacement.y + } + const startFn = d => { + // hide tooltips when drag starts + map.callback_manager.run('hide_tooltip') + } + const dragFn = (d, displacement, totalDisplacement) => { + // draw + moveLabel(d.reaction_id, displacement) + map.draw_these_reactions([ d.reaction_id ]) + } + const endFn = () => {} + const undoFn = (d, displacement) => { + moveLabel(d.reaction_id, utils.c_times_scalar(displacement, -1)) + map.draw_these_reactions([ d.reaction_id ]) + } + const redoFn = (d, displacement) => { + moveLabel(d.reaction_id, displacement) + map.draw_these_reactions([ d.reaction_id ]) + } + return this.getGenericDrag(startFn, dragFn, endFn, undoFn, redoFn, + this.map.sel) } - 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, + getNodeLabelDrag (map) { + const moveLabel = (nodeId, displacement) => { + const node = map.nodes[nodeId] + node.label_x = node.label_x + displacement.x + node.label_y = node.label_y + displacement.y } - var location = { - x: d3_mouse(rel)[0], - y: d3_mouse(rel)[1], + const startFn = d => { + // hide tooltips when drag starts + map.callback_manager.run('hide_tooltip') } + const dragFn = (d, displacement, totalDisplacement) => { + // draw + moveLabel(d.node_id, displacement) + map.draw_these_nodes([ d.node_id ]) + } + const endFn = () => {} + const undoFn = (d, displacement) => { + moveLabel(d.node_id, utils.c_times_scalar(displacement, -1)) + map.draw_these_nodes([ d.node_id ]) + } + const redoFn = (d, displacement) => { + moveLabel(d.node_id, displacement) + map.draw_these_nodes([ d.node_id ]) + } + return this.getGenericDrag(startFn, dragFn, endFn, undoFn, redoFn, + this.map.sel) + } + + /** + * Make a generic drag behavior, with undo/redo. + * + * startFn: function (d) Called at drag start. + * + * dragFn: function (d, displacement, totalDisplacement) Called during drag. + * + * endFn + * + * undoFn + * + * redoFn + * + * relativeToSelection: a d3 selection that the locations are calculated + * against. + * + */ + getGenericDrag (startFn, dragFn, endFn, undoFn, redoFn, + relativeToSelection) { + // define some variables + const behavior = d3Drag() + const undoStack = this.undoStack + const rel = relativeToSelection.node() + let totalDisplacement + + behavior.on('start', d => { + this.dragging = true + + // silence other listeners + d3Selection.event.sourceEvent.stopPropagation() + totalDisplacement = { x: 0, y: 0 } + startFn(d) + }) - // remember the displacement - total_displacement = utils.c_plus_c(total_displacement, displacement) - drag_fn(d, displacement, total_displacement, location) - }.bind(this)) + behavior.on('drag', d => { + // update data + const displacement = { + x: d3Selection.event.dx, + y: d3Selection.event.dy + } + const location = { + x: d3Mouse(rel)[0], + y: d3Mouse(rel)[1] + } - behavior.on('end', function (d) { - this.dragging = false + // remember the displacement + totalDisplacement = utils.c_plus_c(totalDisplacement, displacement) + dragFn(d, displacement, totalDisplacement, location) + }) - // 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], - } + behavior.on('end', d => { + this.dragging = false - undo_stack.push(function () { - // undo - undo_fn(saved_d, saved_displacement, saved_location) - }, function () { - // redo - redo_fn(saved_d, saved_displacement, saved_location) + // add to undo/redo stack + // remember the displacement, dragged nodes, and reactions + const savedD = utils.clone(d) + const savedDisplacement = utils.clone(totalDisplacement) // BUG TODO this variable disappears! + const savedLocation = { + x: d3Mouse(rel)[0], + y: d3Mouse(rel)[1], + } + + undoStack.push(function () { + // undo + undoFn(savedD, savedDisplacement, savedLocation) + }, function () { + // redo + redoFn(savedD, savedDisplacement, savedLocation) + }) + endFn(d) }) - end_fn(d) - }.bind(this)) - return behavior -} + return behavior + } + + /** Make a generic drag behavior, with undo/redo. Supplies angles in place of + * displacements. + * + * startFn: function (d) Called at drag start. + * + * dragFn: function (d, displacement, totalDisplacement) Called during drag. + * + * endFn: + * + * undoFn: + * + * redoFn: + * + * getCenter: + * + * relativeToSelection: a d3 selection that the locations are calculated + * against. + * + */ + getGenericAngularDrag (startFn, dragFn, endFn, undoFn, redoFn, + getCenter, relativeToSelection) { + // define some variables + const behavior = d3Drag() + const undoStack = this.undoStack + const rel = relativeToSelection.node() + let totalAngle + + behavior.on('start', d => { + this.dragging = true -/** 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 + // silence other listeners + d3Selection.event.sourceEvent.stopPropagation() + totalAngle = 0 + startFn(d) + }) - // 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) + behavior.on('drag', d => { + // update data + const displacement = { + x: d3Selection.event.dx, + y: d3Selection.event.dy + } + const location = { + x: d3Mouse(rel)[0], + y: d3Mouse(rel)[1] + } + const center = getCenter() + const angle = utils.angle_for_event(displacement, location, center) + // remember the displacement + totalAngle = totalAngle + angle + dragFn(d, angle, totalAngle, center) }) - end_fn(d) - }.bind(this)) - return behavior + behavior.on('end', d => { + this.dragging = false + + // add to undo/redo stack + // remember the displacement, dragged nodes, and reactions + const savedD = utils.clone(d) + const savedAngle = totalAngle + const savedCenter = utils.clone(getCenter()) + + undoStack.push( + () => undoFn(savedD, savedAngle, savedCenter), + () => redoFn(savedD, savedAngle, savedCenter) + ) + + endFn(d) + }) + + return behavior + } } diff --git a/src/Brush.js b/src/Brush.js index 09e82394..6bd8456a 100644 --- a/src/Brush.js +++ b/src/Brush.js @@ -53,11 +53,11 @@ function brush_is_enabled () { * Turn the brush on or off * @param {Boolean} on_off */ -function toggle (on_off) { - if (on_off === undefined) { - on_off = !this.enabled +function toggle (onOff) { + if (onOff === undefined) { + onOff = !this.enabled } - if (on_off) { + if (onOff) { this.setup_selection_brush() } else { this.brush_sel.selectAll('*').remove() @@ -93,8 +93,8 @@ function setup_selection_brush () { .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) + if (map.settings.get('hide_secondary_metabolites')) { + map.settings.set('hide_secondary_metabolites', false) map.draw_everything() map.set_status('Showing secondary metabolites. You can hide them ' + 'again in Settings.', 2000) diff --git a/src/BuildInput.js b/src/BuildInput.js index d52289b5..575506d7 100644 --- a/src/BuildInput.js +++ b/src/BuildInput.js @@ -1,409 +1,374 @@ -/** 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('./completely') -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)) -} +import utils from './utils' +import PlacedDiv from './PlacedDiv' +import completely from './completely' +import DirectionArrow from './DirectionArrow' +import CobraModel from './CobraModel' +import _ from 'underscore' +import { mouse as d3Mouse } from 'd3-selection' -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() +/** + * BuildInput + * @param selection - A d3 selection for the BuildInput. + * @param map - A Map instance. + * @param zoom_container - A ZoomContainer instance. + * @param settings - A Settings instance. + */ +export default class BuildInput { + constructor (selection, map, zoomContainer, settings) { + // set up container + const newSel = selection.append('div').attr('id', 'rxn-input') + this.placed_div = PlacedDiv(newSel, map, { x: 240, y: 0 }) + this.placed_div.hide() + + // set up complete.ly + this.completely = completely(newSel.node(), { backgroundColor: '#eee' }) + + // close button + newSel.append('button').attr('class', 'button input-close-button') + .text('×') + .on('mousedown', () => this.hideDropdown()) + + // map + this.map = map + // set up the reaction direction arrow + const defaultAngle = 90 // degrees + this.direction_arrow = new DirectionArrow(map.sel) + this.direction_arrow.set_rotation(defaultAngle) + this.setUpMapCallbacks(map) + + // zoom container + this.zoom_container = zoomContainer + this.setUpZoomCallbacks(zoomContainer) + + // settings + this.settings = settings + + // toggle off + this.toggle(false) + this.target_coords = null } -} - -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) -} + setUpMapCallbacks (map) { + // input + map.callback_manager.set('select_metabolite_with_id.input', (selectedNode, coords) => { + if (this.is_active) { + const hasModel = this.reload(selectedNode, coords, false) + if (hasModel) this.showDropdown(coords) + } + this.hideTarget() + }) + map.callback_manager.set('select_selectable.input', (count, selectedNode, coords) => { + this.hideTarget() + if (count === 1 && this.is_active && coords) { + const hasModel = this.reload(selectedNode, coords, false) + if (hasModel) this.showDropdown(coords) + } else { + this.toggle(false) + } + }) + map.callback_manager.set('deselect_nodes', () => { + this.direction_arrow.hide() + this.hideDropdown() + }) + + // svg export + map.callback_manager.set('before_svg_export', () => { + this.direction_arrow.hide() + this.hideTarget() + }) + } -function place(coords) { - this.placed_div.place(coords) - this.direction_arrow.set_location(coords) - this.direction_arrow.show() -} + setUpZoomCallbacks (zoomContainer) { + // TODO this is broken. + // Should place either for selected or for location on zoom or pan. + // zoomContainer.callback_manager.set('zoom_change.input', () => { + // if (this.is_active) { + // this.place_at_selected() + // } + // }) + } -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 -} + is_visible () { // eslint-disable-line camelcase + return this.placed_div.is_visible() + } -/** - * 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 + toggle (onOff) { + if (onOff === undefined) this.is_active = !this.is_active + else this.is_active = onOff + if (this.is_active) { + this.toggleStartReactionListener(true) + let hasModelAndSelection = true + if (_.isNull(this.target_coords)) { + hasModelAndSelection = this.reloadAtSelected() + } else { + this.placed_div.place(this.target_coords) + } + if (hasModelAndSelection) { + this.showDropdown() + this.map.set_status('Click on the canvas or an existing metabolite') + } + this.direction_arrow.show() + } else { + this.toggleStartReactionListener(false) + this.hideDropdown() + this.map.set_status(null) + this.direction_arrow.hide() + } } - this.place(coords) + showDropdown (coords) { + // escape key + this.clear_escape = this.map.key_manager + .add_escape_listener(() => this.hideDropdown(), true) + // dropdown + this.completely.input.blur() + this.completely.repaint() + this.completely.setText('') + this.completely.input.focus() + } - if (this.map.cobra_model===null) { - this.completely.setText('Cannot add: No model.') - return + hideDropdown () { + // 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() } - // 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$2$3') + place (coords) { + this.placed_div.place(coords) + this.direction_arrow.set_location(coords) + this.direction_arrow.show() } - // 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) + /** + * Reload data for autocomplete box and redraw box at the first selected node. + * @return {Boolean} Returns true if a model is present and a node is selected. + */ + reloadAtSelected () { + // get the selected node + this.map.deselect_text_labels() + var selectedNode = this.map.select_single_node() + if (selectedNode === null) return false + var coords = { x: selectedNode.x, y: selectedNode.y } + // reload the reaction input + return this.reload(selectedNode, coords, false) + } - // ignore drawn reactions - if ((!allow_duplicates) && already_drawn(bigg_id, reactions)) { - continue + alreadyDrawn (biggId, reactions) { + for (let drawnId in reactions) { + if (reactions[drawnId].bigg_id === biggId) { + return true + } } + return false + } - // check segments for match to selected metabolite - for (var met_bigg_id in reaction.metabolites) { + /** + * Reload data for autocomplete box and redraw box at the new coordinates. + * @param {} selectedNode - + * @param {} coords - + * @param {Boolean} startingFromScratch - + * @return {Boolean} Returns true if a model is present. + */ + reload (selectedNode, coords, startingFromScratch) { + // Try finding the selected node + if (!startingFromScratch && !selectedNode) { + console.error('No selected node, and not starting from scratch') + return + } - // if starting with a selected metabolite, check for that id - if (starting_from_scratch || met_bigg_id == selected_node.bigg_id) { + this.place(coords) - // don't add suggestions twice - if (bigg_id in reaction_suggestions) continue + if (this.map.cobra_model === null) { + this.completely.setText('Cannot add: No model.') + // this.completely.repaint() + return false + } - var met_name = cobra_metabolites[met_bigg_id].name + // settings + const showNames = this.settings.get('identifiers_on_map') === 'name' + const allowDuplicates = this.settings.get('allow_building_duplicate_reactions') + + // Find selected + const options = [] + const cobraReactions = this.map.cobra_model.reactions + const cobraMetabolites = this.map.cobra_model.metabolites + const reactions = this.map.reactions + const hasDataOnReactions = this.map.hasDataOnReactions + const selectedMetName = (selectedNode ? (showNames ? selectedNode.name : selectedNode.bigg_id) : '') + const boldMetsInStr = (str, mets) => + str.replace(new RegExp('(^| )(' + mets.join('|') + ')($| )', 'g'), '$1$2$3') + + // for reactions + const reactionSuggestions = {} + for (let biggId in cobraReactions) { + const reaction = cobraReactions[biggId] + const reactionName = reaction.name + const showReactionName = (showNames ? reactionName : biggId) + + // ignore drawn reactions + if ((!allowDuplicates) && this.alreadyDrawn(biggId, reactions)) { + continue + } - if (has_data_on_reactions) { - options.push({ reaction_data: reaction.data, - html: ('' + show_r_name + '' + - ': ' + - 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) - } + // check segments for match to selected metabolite + for (let metBiggId in reaction.metabolites) { + // if starting with a selected metabolite, check for that id + if (startingFromScratch || metBiggId === selectedNode.biggId) { + // don't add suggestions twice + if (biggId in reactionSuggestions) continue + + if (hasDataOnReactions) { + options.push({ + reaction_data: reaction.data, + html: '' + showReactionName + '' + ': ' + reaction.data_string, + matches: [showReactionName], + id: biggId + }) + reactionSuggestions[biggId] = true } else { - mets = utils.clone(reaction.metabolites) - for (met_id in reaction.metabolites) { - show_met_names.push(met_id) + // get the metabolite names or IDs + let mets = {} + const showMetNames = [] + let metId + if (showNames) { + for (metId in reaction.metabolites) { + var name = cobraMetabolites[metId].name + mets[name] = reaction.metabolites[metId] + showMetNames.push(name) + } + } else { + mets = utils.clone(reaction.metabolites) + for (metId in reaction.metabolites) { + showMetNames.push(metId) + } } + const showGeneNames = _.flatten( + reaction.genes.map(g => [ g.name, g.biggId ]) + ) + // get the reaction string + const reactionString = CobraModel.build_reaction_string(mets, + reaction.reversibility, + reaction.lower_bound, + reaction.upper_bound) + // make the matches list and filter out any missing entries (e.g. + // missing gene names from model + const matches = [ showReactionName ].concat(showMetNames).concat(showGeneNames).filter(x => x) + options.push({ + html: ('' + showReactionName + '' + '\t' + + boldMetsInStr(reactionString, [selectedMetName])), + matches, + id: biggId + }) + reactionSuggestions[biggId] = true } - 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: ('' + show_r_name + '' + '\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 + // Generate the array of reactions to suggest and sort it + const sortFn = hasDataOnReactions + ? (x, y) => Math.abs(y.reactionData) - Math.abs(x.reactionData) + : (x, y) => x.html.toLowerCase() < y.html.toLowerCase() ? -1 : 1 + + // set up the box with data + this.completely.options = options.sort(sortFn) + + // TODO test this behavior + // if (strings_to_display.length==1) this.completely.setText(strings_to_display[0]) + // else this.completely.setText("") + this.completely.setText('') + + const checkAndBuild = id => { + if (id !== null) { + // make sure the selected node exists, in case changes were made in the meantime + if (startingFromScratch) { + this.map.new_reaction_from_scratch(id, + coords, + this.direction_arrow.get_rotation()) + } else { + if (!(selectedNode.node_id in this.map.nodes)) { + console.error('Selected node no longer exists') + this.hideDropdown() + return + } + this.map.new_reaction_for_metabolite(id, + selectedNode.node_id, + this.direction_arrow.get_rotation()) } - 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 + this.completely.onEnter = function (id) { + this.setText('') + this.onChange('') + checkAndBuild(id) } - 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 + return true } - 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() + /** + * Toggle listening for a click to place a new reaction on the canvas. + */ + toggleStartReactionListener (onOff) { + if (onOff === undefined) { + this.start_reaction_listener = !this.start_reaction_listener + } else if (this.start_reaction_listener === onOff) { + return + } else { + this.start_reaction_listener = onOff + } + + if (this.start_reaction_listener) { + const node = this.map.sel.node() + this.map.sel.on('click.start_reaction', () => { + // TODO fix this hack + if (this.direction_arrow.dragging) return + // reload the reaction input + var coords = { + x: d3Mouse(node)[0], + y: d3Mouse(node)[1] + } + // unselect metabolites + this.map.deselect_nodes() + this.map.deselect_text_labels() + // reload the reaction input + const hasModel = this.reload(null, coords, true) + if (hasModel) { + // show the dropdown + this.showDropdown(coords) + } + // generate the target symbol + this.showTarget(this.map, coords) + }) + 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.hideTarget() + } } -} -function hide_target () { - if (this.target_coords) { - this.map.sel.selectAll('.start-reaction-target').remove() + hideTarget () { + if (this.target_coords) { + this.map.sel.selectAll('.start-reaction-target').remove() + } + this.target_coords = null } - this.target_coords = null -} -function show_target (map, coords) { - var s = map.sel.selectAll('.start-reaction-target').data([12, 5]) - s.enter() - .append('circle') + showTarget (map, coords) { + var s = map.sel.selectAll('.start-reaction-target').data([12, 5]) + s.enter() + .append('circle') .classed('start-reaction-target', true) .attr('r', function (d) { return d }) .style('stroke-width', 4) .merge(s) - .style('visibility', 'visible') - .attr('transform', 'translate(' + coords.x + ',' + coords.y + ')') - this.target_coords = coords + .style('visibility', 'visible') + .attr('transform', 'translate(' + coords.x + ',' + coords.y + ')') + this.target_coords = coords + } } diff --git a/src/Builder.css b/src/Builder.css index 242deeb1..7be819c0 100644 --- a/src/Builder.css +++ b/src/Builder.css @@ -45,12 +45,22 @@ resizes. */ } /* The zoom container classes. */ -.escher-zoom-container, .escher-3d-transform-container, svg.escher-svg { +.escher-container .escher-zoom-container, +.escher-container .escher-3d-transform-container, +.escher-container svg.escher-svg { width: 100% !important; height: 100% !important; overflow: hidden; } +/* SVG text should not be selectable */ +.escher-container svg text { + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + /* Status */ .escher-container #status { position:absolute; @@ -60,51 +70,8 @@ resizes. */ background-color: white; font-size: 16px } -/* Menus */ -.escher-container #menu { - display: block; - margin: 5px auto 0 auto; - border: 1px solid #ddd; - background-color: rgba(255, 255, 255, 0.95); - border-radius: 0px; -} -.escher-container .help-button { - float: right; - padding: 0 5px; - font-size: 12px; - background-color: rgb(245, 245, 245); -} -.escher-container .dropdown-menu { - border: 1px solid #ddd; - background-color: rgba(255, 255, 255, 0.95); - border-radius: 0px; - margin: 0; - text-align: left; -} -.escher-container .dropdown-menu>li>a { - font-size: 15px; -} -.escher-container .dropdown-menu>.escher-disabled>a { - color: #E0E0E0; - pointer-events: none; -} -.escher-container .dropdown { - background-color: rgba(255, 255, 255, 0.95); -} -@media (max-width: 550px) { - .escher-container .dropdown-button { - padding: 5px 9px; - } -} -@media (min-width: 550px) { - .escher-container .dropdown-button { - font-size: 18px; - } - .escher-container .help-button { - font-size: 16px; - } -} -/* Search */ + +/* Search & Menu */ .escher-container .search-menu-container { position: absolute; width: 100%; @@ -126,240 +93,30 @@ resizes. */ width: 410px; } } -.escher-container .search-container { - display: block; - background: rgba(255, 255, 255, 0.95); - padding: 3px; - border: 1px solid #DDD; - margin: 2px 0 0 0; -} -.escher-container .search-bar { - display: inline-block; - border: 1px solid #DDD; - margin-right: 4px; - width: 114px; - height: 29px; - border-radius: 3px; -} -.escher-container .search-counter { - display: inline-block; - padding: 0 8px; -} -/* Settings */ -.escher-container .settings-box-background { - display: block; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: 9; - background: rgba(0, 0, 0, 0.4); - padding: 5% 0 5% 0; - text-align: center; -} -.escher-container .settings-box-container { - display: inline-block; - position: relative; - width: 90%; - max-width: 520px; - height: 100%; - z-index: 10; -} -.escher-container .settings-button { - position: absolute; - top: 5px; - right: 20px; - z-index: 10; -} -.escher-container .settings-button-close { - right: 60px; -} -.escher-container .settings-box { - display: inline-block; - max-height: 100%; - width: 100%; - /* max-width: 520px; */ - overflow-y: scroll; - overflow-x: hidden; - background: rgba(255, 255, 255, 0.95); - padding: 8px; - margin: 0; - border: 1px solid #DDD; - text-align: left; -} -.escher-container .settings-section-heading-large { - font-size: 17px; - font-weight: bold; - margin-top: 15px; -} -.escher-container .settings-section-heading { - font-weight: bold; - margin-top: 15px; -} -.escher-container .settings-table { - width: 100%; -} -.escher-container .settings-table td { - padding: 3px 5px; -} -.escher-container td.options-label { - width: 88px; -} -.escher-container td.options-label-wide { - width: 192px; -} -.escher-container .settings-number { - text-align: center; - color: #AAA; - font-style: italic; -} -.escher-container .input-cell { - width: 23%; - /* padding: 3px 5px; */ - font-size: 13px; -} -.escher-container .scale-bar-input { - width: 100%; - text-align: left; -} -.escher-container .no-data-input { - width: 23%; - margin: 0 4px 0 4px; -} -.escher-container label.option-group { - margin: 0 10px 0 0; -} -.escher-container label.option-group span { - margin: 0 0 0 4px; -} -.escher-container label.full-line { - width: 100%; -} -.escher-container label { - font-weight: inherit; -} -.escher-container .settings-tip { - font-style: italic; -} -/* Live scale */ -.escher-container .scale-editor { - position: relative; - width: 100%; - height: 190px; - font-size: 12px; -} -.escher-container .scale-editor .centered { - position: absolute; - width: 440px; - left: 35px; - top: 0px; -} -.escher-container .scale-editor .data-not-loaded { - position: absolute; - z-index: 5; - left: 60px; - top: 25px; - text-align: center; - width: 400px; - font-size: 15px; - color: #B19EC0; -} -.escher-container .scale-editor .add { - position: absolute; - top: 0px; - right: 0px; - width: 20px; - cursor: pointer; -} -.escher-container .scale-editor .trash-container { - position: absolute; - top: 0px; - left: 0px; - width: 420px; - height: 20px; -} -.escher-container .scale-editor .trash { - position: absolute; - top: 0px; - left: 0px; - cursor: pointer; -} -.escher-container .scale-editor .scale-svg { - position: absolute; - width: 440px; - height: 80px; - top: 20px; - left: 0px; -} -.escher-container .scale-editor .input-container { - position: absolute; - top: 56px; - left: 0px; - width: 420px; -} -.escher-container .scale-editor .input-container input { - position: absolute; - left: 0px; - top: 0px; -} -.escher-container .scale-editor input[disabled] { - background-color: #F1ECFA; -} -.escher-container .scale-editor .row-label { - position: absolute; - left: 0px; -} -.escher-container .scale-editor .input-set { - position: absolute; - top: 0px; - box-shadow: 0px 2px 14px rgb(197, 197, 197); -} -.escher-container .scale-editor .input-set.selected-set { - z-index: 5; -} -.escher-container .scale-editor .domain-type-picker { - position: absolute; - top: 0px; - right: 0px; - height: 14px; - background-color: rgb(232, 232, 232); -} -.escher-container .scale-editor .picker rect { - color: black; - opacity: 0.4; -} -.escher-container .scale-editor .no-data { - position: absolute; - left: 0px; - width: 100%; -} -.escher-container .scale-editor .no-data-heading { - font-weight: bold; -} -.escher-container .scale-editor .no-data .input-group { - position: absolute; - top: 0px; - left: 0px; -} -.escher-container .scale-editor .no-data .input-group span, -.escher-container .scale-editor .no-data .input-group input { - position: absolute; - top: 17px; -} /* Reaction input */ .escher-container #rxn-input { z-index: 10; + width: 200px; +} +.escher-container .input-close-button { + position: absolute; + right: 0px; + width: 18px; + bottom: 0px; + padding: 0px; + border-width: 0px; + margin: 0px; + background: none; + font-size: 20px; + font-weight: normal; + top: -8px; } .escher-container .input-close-button:hover { color: #ff3333; font-weight: bold; } -.escher-container .light { - color: #8b8b8b; - font-weight: normal; -} + /* text edit input */ .escher-container #text-edit-input input { width: 500px; diff --git a/src/Builder.jsx b/src/Builder.jsx index 62427022..ec77b6dc 100644 --- a/src/Builder.jsx +++ b/src/Builder.jsx @@ -3,12 +3,6 @@ */ /** @jsx h */ -import preact, { h } from 'preact' -import ReactWrapper from './ReactWrapper' -import BuilderSettingsMenu from './BuilderSettingsMenu' -import ButtonPanel from './ButtonPanel' -import BuilderMenuBar from './BuilderMenuBar' -import SearchBar from './SearchBar' import * as utils from './utils' import BuildInput from './BuildInput' import ZoomContainer from './ZoomContainer' @@ -20,6 +14,11 @@ import Settings from './Settings' import TextEditInput from './TextEditInput' import QuickJump from './QuickJump' import dataStyles from './data_styles' +import renderWrapper from './renderWrapper' +import SettingsMenu from './SettingsMenu' +import MenuBar from './MenuBar' +import SearchBar from './SearchBar' +import ButtonPanel from './ButtonPanel' import TooltipContainer from './TooltipContainer' import DefaultTooltip from './DefaultTooltip' import _ from 'underscore' @@ -34,10 +33,11 @@ import './Builder.css' // Import CSS as a string to embed. This also works from lib because css/src get // uploaded to NPM. -import builder_embed from '!!raw-loader!./Builder-embed.css' +// eslint-disable-next-line import/no-webpack-loader-syntax +import builderEmbed from '!!raw-loader!./Builder-embed.css' class Builder { - constructor (map_data, model_data, embedded_css, selection, options) { + constructor (mapData, modelData, embeddedCss, selection, options) { // Defaults if (!selection) { selection = d3Select('body').append('div') @@ -54,21 +54,20 @@ class Builder { if (!options) { options = {} } - if (!embedded_css) { - embedded_css = builder_embed + if (!embeddedCss) { + embeddedCss = builderEmbed } - this.map_data = map_data - this.model_data = model_data - this.embedded_css = embedded_css + this.map_data = mapData + this.model_data = modelData + this.embeddedCss = embeddedCss this.selection = selection this.menu_div = null this.button_div = null - this.settings_div = null - this.settingsMenuRef = null this.search_bar_div = null this.searchBarRef = null this.semanticOptions = null + this.mode = 'zoom' // apply this object as data for the selection this.selection.datum(this) @@ -78,11 +77,11 @@ class Builder { this.has_custom_reaction_styles = Boolean(options.reaction_styles) // set defaults - this.options = utils.set_options(options, { + const optionsWithDefaults = utils.set_options(options, { // view options menu: 'all', scroll_behavior: 'pan', - use_3d_transform: !utils.check_browser('safari'), + use_3d_transform: false, enable_editing: true, enable_keys: true, enable_search: true, @@ -136,7 +135,8 @@ class Builder { ], // Extensions tooltip_component: DefaultTooltip, - enable_tooltips: ['label', 'object'], + enable_tooltips: ['label'], + enable_keys_with_tooltip: true, reaction_scale_preset: null, metabolite_scale_preset: null, // Callbacks @@ -156,21 +156,6 @@ class Builder { 'because UI elements are html-based.') } - // Warn if scales are too short - ;['reaction_scale', 'metabolite_scale'].map(scaleType => { - if (this.options[scaleType] && this.options[scaleType].length < 2) { - console.warn(`Bad value for option "${scaleType}". Scales must have at least 2 points.`) - } - }) - - // 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 = [ 'identifiers_on_map', @@ -195,92 +180,89 @@ class Builder { 'metabolite_no_data_color', 'metabolite_no_data_size' ] - this.settings = new Settings(set_option, get_option, conditional) + // this.options and this.settings used to have different functions, but now + // they are aliases + this.settings = new Settings(optionsWithDefaults, conditional) + + // Warn if scales are too short + ;['reaction_scale', 'metabolite_scale'].map(scaleType => { + if (this.settings.get(scaleType) && this.settings.get(scaleType).length < 2) { + console.warn(`Bad value for option "${scaleType}". Scales must have at least 2 points.`) + } + }) // 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) + const firstLoadCallback = this.settings.get('first_load_callback') + if (firstLoadCallback !== null) { + this.callback_manager.set('first_load', () => { + firstLoadCallback(this) + }) } // 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) + this.settings.get('scroll_behavior'), + this.settings.get('use_3d_transform'), + this.settings.get('fill_screen')) // Zoom container status changes - this.zoom_container.callback_manager.set('svg_start', function () { - if (this.map) this.map.set_status('Drawing ...') - }.bind(this)) - this.zoom_container.callback_manager.set('svg_finish', function () { - if (this.map) this.map.set_status('') - }.bind(this)) - this.zoom_container.callback_manager.set('zoomChange', function () { - if (this.options.semantic_zoom) { + // this.zoom_container.callback_manager.set('svg_start', () => { + // if (this.map) this.map.set_status('Drawing ...') + // }) + // this.zoom_container.callback_manager.set('svg_finish', () => { + // if (this.map) this.map.set_status('') + // }) + this.zoom_container.callback_manager.set('zoom_change', () => { + if (this.settings.get('semantic_zoom')) { const scale = this.zoom_container.window_scale - const optionObject = this.options.semantic_zoom - .sort((a, b) => a.zoomLevel - b.zoomLevel) - .find(a => a.zoomLevel > scale) + const optionObject = this.settings.get('semantic_zoom') + .sort((a, b) => a.zoomLevel - b.zoomLevel) + .find(a => a.zoomLevel > scale) if (optionObject) { - Object.entries(optionObject.options).map(([option, value]) => { - if (this.options[option] !== value) { - this.settings.set_conditional(option, value) - this._update_data(false, true) + let didChange = false + _.mapObject(optionObject.options, (value, key) => { + if (this.settings.get(key) !== value) { + this.settings.set(key, value) + didChange = true } }) + if (didChange) this._updateData(false, true) } } - }.bind(this)) - - // Set up the tooltip container - this.tooltip_container = new TooltipContainer(this.selection, - this.options.tooltip_component, - this.zoom_container) + }) + this.settings.streams.use_3d_transform.onValue(val => { + this.zoom_container.set_use_3d_transform(val) + }) // Status in both modes - this._create_status(this.selection) + this._createStatus(this.selection) // Load the model, map, and update data in both this.load_model(this.model_data, false) // Append the bars and menu divs to the document var s = this.selection - .append('div').attr('class', 'search-menu-container') - .append('div').attr('class', 'search-menu-container-inline') + .append('div').attr('class', 'search-menu-container') + .append('div').attr('class', 'search-menu-container-inline') this.menu_div = s.append('div') this.search_bar_div = s.append('div') this.button_div = this.selection.append('div') - this.settings_div = this.selection.append('div') // Need to defer map loading to let webpack CSS load properly _.defer(() => { this.load_map(this.map_data, false) - const message_fn = this._reaction_check_add_abs() - if (message_fn !== null) { - this._update_data(true, true) - } - - _.mapObject(this.settings.streams, (stream, key) => { - stream.onValue(value => { - this.pass_settings_menu_props({ - ...this.options, - map: this.map, - settings: this.settings - }) - if (key === 'reaction_styles' || key === 'metabolite_styles') { - this._update_data(false, true) - } - }) - }) + const messageFn = this._reactionCheckAddAbs() + this._updateData(true, true) // Setting callbacks. TODO enable atomic updates. Right now, every time the // menu closes, everything is drawn. - this.settings.status_bus.onValue(x => { + this.settings.statusBus.onValue(x => { if (x === 'accept') { - this._update_data(true, true, [ 'reaction', 'metabolite' ], false) + this._updateData(true, true, [ 'reaction', 'metabolite' ], false) if (this.zoom_container !== null) { - const new_behavior = this.settings.get_option('scroll_behavior') - this.zoom_container.set_scroll_behavior(new_behavior) + // TODO make this automatic + const newBehavior = this.settings.get('scroll_behavior') + this.zoom_container.set_scroll_behavior(newBehavior) } if (this.map !== null) { this.map.draw_all_nodes(false) @@ -293,108 +275,103 @@ class Builder { // Set up quick jump this._setup_quick_jump(this.selection) - if (message_fn !== null) setTimeout(message_fn, 500) + if (messageFn !== null) setTimeout(messageFn, 500) // Finally run callback _.defer(() => this.callback_manager.run('first_load', this)) }) } + // builder.options is deprecated + get options () { + throw new Error('builder.options is deprecated. Use builder.settings.get() ' + + 'and builder.settings.set() instead.') + } + set options (_) { + throw new Error('builder.options is deprecated. Use builder.settings.get() ' + + 'and builder.settings.set() instead.') + } + /** * For documentation of this function, see docs/javascript_api.rst. */ - load_model (model_data, should_update_data) { - if (_.isUndefined(should_update_data)) { - should_update_data = true - } - + load_model (modelData, shouldUpdateData = true) { // eslint-disable-line camelcase // Check the cobra model - if (_.isNull(model_data)) { + if (_.isNull(modelData)) { this.cobra_model = null } else { - this.cobra_model = CobraModel.from_cobra_json(model_data) + this.cobra_model = CobraModel.from_cobra_json(modelData) } if (this.map) { this.map.cobra_model = this.cobra_model - if (should_update_data) { - this._update_data(true, false) + if (shouldUpdateData) { + this._updateData(true, false) } - if (this.settings.get_option('highlight_missing')) { + if (this.settings.get('highlight_missing')) { this.map.draw_all_reactions(false, false) } } - this.callback_manager.run('load_model', null, model_data, should_update_data) + this.callback_manager.run('load_model', null, modelData, shouldUpdateData) } /** * For documentation of this function, see docs/javascript_api.rst */ - load_map (map_data, should_update_data) { - if (_.isUndefined(should_update_data)) { - should_update_data = true - } - + load_map (mapData, shouldUpdateData = true) { // eslint-disable-line camelcase // Store map options that might be changed by semantic_zoom function const tempSemanticOptions = {} - if (this.options.semantic_zoom) { - for (let level of this.options.semantic_zoom) { + if (this.settings.get('semantic_zoom')) { + for (let level of this.settings.get('semantic_zoom')) { Object.keys(level.options).map(option => { if (tempSemanticOptions[option] === undefined) { - tempSemanticOptions[option] = this.options[option] + tempSemanticOptions[option] = this.settings.get(option) } }) } this.semanticOptions = Object.assign({}, tempSemanticOptions) } - // Begin with some definitions - var selectable_mousedown_enabled = true - var shift_key_on = false - // remove the old builder utils.remove_child_nodes(this.zoom_container.zoomed_sel) - var zoomed_sel = this.zoom_container.zoomed_sel - var svg = this.zoom_container.svg + const zoomedSel = this.zoom_container.zoomed_sel + const svg = this.zoom_container.svg // remove the old map side effects if (this.map) { this.map.key_manager.toggle(false) } - if (map_data !== null) { + if (mapData !== null) { // import map - this.map = Map.from_data(map_data, + this.map = Map.from_data(mapData, svg, - this.embedded_css, - zoomed_sel, + this.embeddedCss, + zoomedSel, this.zoom_container, this.settings, this.cobra_model, - this.options.enable_search) + this.settings.get('enable_search')) } else { // new map this.map = new Map(svg, - this.embedded_css, - zoomed_sel, + this.embeddedCss, + zoomedSel, this.zoom_container, this.settings, this.cobra_model, - this.options.canvas_size_and_loc, - this.options.enable_search) + this.settings.get('canvas_size_and_loc'), + this.settings.get('enable_search')) } // Connect status bar this._setup_status(this.map) - // Connect status bar - this._setup_status(this.map) - // Set the data for the map - if (should_update_data) { - this._update_data(false, true) + if (shouldUpdateData) { + this._updateData(false, true) } // Set up the reaction input with complete.ly @@ -405,98 +382,96 @@ class Builder { this.text_edit_input = new TextEditInput(this.selection, this.map, this.zoom_container) - // Connect the tooltip - this.tooltip_container.setup_map_callbacks(this.map) - // 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)) + this.brush = new Brush(zoomedSel, false, this.map, '.canvas-group') + // reset brush when canvas resizes in brush mode + this.map.canvas.callback_manager.set('resize', () => { + if (this.mode === 'brush') this.brush.toggle(true) + }) // Set up the modes - this._setup_modes(this.map, this.brush, this.zoom_container) - - // Set up settings menu - preact.render( - { this.settingsMenuRef = instance }} - closeMenu={() => this.pass_settings_menu_props({display: false})} - />, - this.settings_div.node(), - this.settings_div.node().children.length > 0 - ? this.settings_div.node().firstChild - : undefined - ) + this._setUpModes(this.map, this.brush, this.zoom_container) + + // Set up menus + this.setUpSettingsMenu() + // share a parent container for menu bar and search bar + const sel = this.selection + .append('div').attr('class', 'search-menu-container') + .append('div').attr('class', 'search-menu-container-inline') + this.setUpMenuBar(sel) + this.setUpSearchBar(sel) + this.setUpButtonPanel() - if (this.options.enable_search) { - this.renderSearchBar(true) - } - - // Set up key manager - var keys = this._get_keys( - this.map, + // Set up the tooltip container + this.tooltip_container = new TooltipContainer( + this.selection, + this.settings.get('tooltip_component'), this.zoom_container, - this.search_bar, - this.settings_bar, - this.options.enable_editing, - this.options.full_screen_button + this.map ) + + // Set up key manager + const keys = this.getKeys() 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.searchBarRef, () => this.settingsMenuRef, - this.text_edit_input, - this.tooltip_container + this.text_edit_input ] + if (!this.settings.get('enable_keys_with_tooltip')) { + this.map.key_manager.input_list.push(this.tooltip_container) + } + // 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) + this.map.key_manager.toggle(this.settings.get('enable_keys')) // Disable clears - if (this.options.disabled_buttons === null) { - this.options.disabled_buttons = []; + const newDisabledButtons = this.settings.get('disabled_buttons') || [] + if (!this.settings.get('reaction_data')) { + newDisabledButtons.push('Clear reaction data') + } + if (!this.settings.get('gene_data')) { + newDisabledButtons.push('Clear gene data') } - if (!this.options.reaction_data && this.options.disabled_buttons) { - this.options.disabled_buttons.push('Clear reaction data') + if (!this.settings.get('metabolite_data')) { + newDisabledButtons.push('Clear metabolite data') } - if (!this.options.gene_data && this.options.disabled_buttons) { - this.options.disabled_buttons.push('Clear gene data') + if (!this.settings.get('enable_search')) { + newDisabledButtons.push('Find') } - if (!this.options.metabolite_data && this.options.disabled_buttons) { - this.options.disabled_buttons.push('Clear metabolite data') + if (!this.settings.get('enable_editing')) { + newDisabledButtons.push('Show control points') } + this.settings.set('disabled_buttons', newDisabledButtons) - // Setup selection box - if (this.options.zoom_to_element) { - const type = this.options.zoom_to_element.type - const element_id = this.options.zoom_to_element.id + // Set up selection box + if (this.settings.get('zoom_to_element')) { + const type = this.settings.get('zoom_to_element').type + const elementId = this.settings.get('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)) { + if (_.isUndefined(elementId)) { throw new Error('zoom_to_element must include id') } if (type === 'reaction') { - this.map.zoom_to_reaction(element_id) + this.map.zoom_to_reaction(elementId) } else if (type === 'node') { - this.map.zoom_to_node(element_id) + this.map.zoom_to_node(elementId) } - } else if (map_data !== null) { + } else if (mapData !== null) { this.map.zoom_extent_canvas() } else { - if (this.options.starting_reaction !== null && this.cobra_model !== null) { + if (this.settings.get('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) + const size = this.zoom_container.get_size() + const startCoords = { x: size.width / 2, y: size.height / 4 } + this.map.new_reaction_from_scratch(this.settings.get('starting_reaction'), + startCoords, 90) this.map.zoom_extent_nodes() } else { this.map.zoom_extent_canvas() @@ -504,14 +479,14 @@ class Builder { } // Start in zoom mode for builder, view mode for viewer - if (this.options.enable_editing) { + if (this.settings.get('enable_editing')) { this.zoom_mode() } else { this.view_mode() } // confirm before leaving the page - if (this.options.enable_editing) { + if (this.settings.get('enable_editing')) { this._setup_confirm_before_exit() } @@ -519,114 +494,230 @@ class Builder { this.map.draw_everything() } - renderMenu (mode) { - const menuDivNode = this.menu_div.node() - if (this.options.menu === 'all') { - preact.render( - { - // Revert options changed by semanticZoom to their original values if option is active - if (this.semanticOptions) { - Object.entries(this.semanticOptions).map(([key, value]) => { - this.settings.set_conditional(key, value) - }) - this._update_data() - } - this.map.save() - }} - loadMap={(file) => this.load_map(file)} - saveSvg={() => this.map.save_svg()} - savePng={() => this.map.save_png()} - clearMap={() => this.map.clear_map()} - loadModel={file => this.load_model(file, true)} - updateRules={() => this.map.convert_map()} - loadReactionData={file => { - this.set_reaction_data(file) - this._set_mode(mode) - }} - loadGeneData={file => { - this.set_gene_data(file) - this._set_mode(mode) - }} - loadMetaboliteData={file => { - this.set_metabolite_data(file) - this._set_mode(mode) - }} - setMode={(newMode) => this._set_mode(newMode)} - deleteSelected={() => this.map.delete_selected()} - undo={() => this.map.undo_stack.undo()} - redo={() => this.map.undo_stack.redo()} - togglePrimary={() => this.map.toggle_selected_node_primary()} - cyclePrimary={() => this.map.cycle_primary_node()} - selectAll={() => this.map.select_all()} - selectNone={() => this.map.select_none()} - invertSelection={() => this.map.invert_selection()} - zoomIn={() => this.zoom_container.zoom_in()} - zoomOut={() => this.zoom_container.zoom_out()} - zoomExtentNodes={() => this.map.zoom_extent_nodes()} - zoomExtentCanvas={() => this.map.zoom_extent_canvas()} - search={() => this.renderSearchBar()} - toggleBeziers={() => this.map.toggle_beziers()} - renderSettingsMenu={() => this.pass_settings_menu_props({ - ...this.options, - map: this.map, - settings: this.settings, - display: true - })} - />, - menuDivNode, - menuDivNode.children.length > 0 // If there is already a div, re-render it. Otherwise make a new one - ? menuDivNode.firstChild - : undefined - ) - } + /** + * Function to pass props for the settings menu. Run without an argument to + * rerender the component + * @param {Object} props - Props that the settings menu will use + */ + passPropsSettingsMenu (props = {}) { + this.callback_manager.run('passPropsSettingsMenu', null, props) + } + + /** + * Initialize the settings menu + */ + setUpSettingsMenu () { + this.settingsMenuRef = null + renderWrapper( + SettingsMenu, + instance => { this.settingsMenuRef = instance }, + passProps => this.callback_manager.set('passPropsSettingsMenu', passProps), + this.selection.append('div').node() + ) + this.passPropsSettingsMenu({ + display: false, + settings: this.settings, + map: this.map + }) + + // redraw menu when settings change + _.mapObject(this.settings.streams, (stream, key) => { + stream.onValue(value => { + this.passPropsSettingsMenu() + }) + }) + + // recalculate data when switching to/from absolute value + this.settings.streams.reaction_styles + .map(x => _.contains(x, 'abs')) + .skipDuplicates() + .onValue(() => this._updateData(false, true)) + this.settings.streams.metabolite_styles + .map(x => _.contains(x, 'abs')) + .skipDuplicates() + .onValue(() => this._updateData(false, true)) + } + + /** + * Function to pass props for the menu bar + * @param {Object} props - Props that the menu bar will use + */ + passPropsMenuBar (props = {}) { + this.callback_manager.run('passPropsMenuBar', null, props) + } + + /** + * Initialize the menu bar + * @param {D3 Selection} sel - The d3 selection to render in. + */ + setUpMenuBar (sel) { + this.menuBarRef = null + renderWrapper( + MenuBar, + instance => { this.menuBarRef = instance }, + passProps => this.callback_manager.set('passPropsMenuBar', passProps), + sel.append('div').node() + ) + this.passPropsMenuBar({ + display: this.settings.get('menu') === 'all', + settings: this.settings, + sel: this.selection, + mode: this.mode, + map: this.map, + saveMap: () => { + // Revert options changed by semanticZoom to their original values if option is active + if (this.semanticOptions) { + Object.entries(this.semanticOptions).map(([key, value]) => { + this.settings.set(key, value) + }) + this._updateData() + } + this.map.save() + }, + loadMap: (file) => this.load_map(file), + saveSvg: () => this.map.save_svg(), + savePng: () => this.map.save_png(), + clearMap: () => { + this.map.clear_map() + this.callback_manager.run('clear_map') + }, + loadModel: file => this.load_model(file, true), + clearModel: () => { + this.load_model(null) + this.callback_manager.run('clear_model') + }, + updateRules: () => this.map.convert_map(), + setReactionData: d => this.set_reaction_data(d), + setGeneData: d => this.set_gene_data(d), + setMetaboliteData: d => this.set_metabolite_data(d), + setMode: mode => this._setMode(mode), + deleteSelected: () => this.map.delete_selected(), + undo: () => this.map.undo_stack.undo(), + redo: () => this.map.undo_stack.redo(), + togglePrimary: () => this.map.toggle_selected_node_primary(), + cyclePrimary: () => this.map.cycle_primary_node(), + selectAll: () => this.map.select_all(), + selectNone: () => this.map.select_none(), + invertSelection: () => this.map.invert_selection(), + zoomIn: () => this.zoom_container.zoom_in(), + zoomOut: () => this.zoom_container.zoom_out(), + zoomExtentNodes: () => this.map.zoom_extent_nodes(), + zoomExtentCanvas: () => this.map.zoom_extent_canvas(), + search: () => this.passPropsSearchBar({ display: true }), + toggleBeziers: () => this.map.toggle_beziers(), + renderSettingsMenu: () => this.passPropsSettingsMenu({ display: true }) + }) + + // redraw when beziers change + this.map.callback_manager.set('toggle_beziers', () => { + this.passPropsMenuBar() + }) + + // redraw when disabledButtons change + this.settings.streams.disabled_buttons.onValue(value => { + this.passPropsMenuBar() + }) + + // redraw when mode changes + this.callback_manager.set('set_mode', mode => { + this.passPropsMenuBar({ mode }) + }) + } + + /** + * Function to pass props for the search bar + * @param {Object} props - Props that the search bar will use + */ + passPropsSearchBar (props = {}) { + this.callback_manager.run('passPropsSearchBar', null, props) } - renderSearchBar (hide, searchItem) { - if (!this.options.enable_search) { return } - const searchBarNode = this.search_bar_div.node() - preact.render( - { this.searchBarRef = instance }} - />, - searchBarNode, - searchBarNode.children.length > 0 // If there is already a div, re-render it. Otherwise make a new one - ? searchBarNode.firstChild - : undefined + /** + * Initialize the search bar + * @param {D3 Selection} sel - The d3 selection to render in. + */ + setUpSearchBar (sel) { + this.searchBarRef = null + renderWrapper( + SearchBar, + instance => { this.searchBarRef = instance }, + passProps => this.callback_manager.set('passPropsSearchBar', passProps), + sel.append('div').node() ) + this.passPropsSearchBar({ + display: false, + searchIndex: this.map.search_index, + map: this.map + }) } - renderButtonPanel (mode) { - const buttonPanelDivNode = this.button_div.node() - preact.render( - this._set_mode(newMode)} - zoomContainer={this.zoom_container} - map={this.map} - mode={mode} - buildInput={this.build_input} - />, - buttonPanelDivNode, - buttonPanelDivNode.children.length > 0 // If there is already a div, re-render it. Otherwise make a new one - ? buttonPanelDivNode.firstChild - : undefined + /** + * Function to pass props for the button panel + * @param {Object} props - Props that the tooltip will use + */ + passPropsButtonPanel (props = {}) { + this.callback_manager.run('passPropsButtonPanel', null, props) + } + + /** + * Initialize the button panel + */ + setUpButtonPanel () { + renderWrapper( + ButtonPanel, + null, + passProps => this.callback_manager.set('passPropsButtonPanel', passProps), + this.selection.append('div').node() ) + this.passPropsButtonPanel({ + display: _.contains(['all', 'zoom'], this.settings.get('menu')), + mode: this.mode, + settings: this.settings, + setMode: mode => this._setMode(mode), + zoomContainer: this.zoom_container, + map: this.map, + buildInput: this.build_input + }) + // redraw when mode changes + this.callback_manager.set('set_mode', mode => { + this.passPropsButtonPanel({ mode }) + }) } - _set_mode (mode) { - this.renderMenu(mode) - this.renderButtonPanel(mode) + /** + * Set up callbacks for the rotation mode + */ + _setUpModes (map, brush, zoomContainer) { + // set up zoom+pan and brush modes + var wasEnabled = {} + map.callback_manager.set('start_rotation', function () { + wasEnabled.brush = brush.enabled + brush.toggle(false) + wasEnabled.zoom = zoomContainer.zoom_on + zoomContainer.toggle_pan_drag(false) + wasEnabled.selectableMousedown = map.behavior.selectableMousedown !== null + map.behavior.toggleSelectableClick(false) + wasEnabled.labelMouseover = map.behavior.labelMouseover !== null + wasEnabled.labelTouch = map.behavior.labelTouch !== null + map.behavior.toggleLabelMouseover(false) + map.behavior.toggleLabelTouch(false) + }) + map.callback_manager.set('end_rotation', function () { + brush.toggle(wasEnabled.brush) + zoomContainer.toggle_pan_drag(wasEnabled.zoom) + map.behavior.toggleSelectableClick(wasEnabled.selectableMousedown) + map.behavior.toggleLabelMouseover(wasEnabled.labelMouseover) + map.behavior.toggleLabelTouch(wasEnabled.labelTouch) + wasEnabled = {} + }) + } + + /** + * Set the mode + */ + _setMode (mode) { + this.mode = mode + // input this.build_input.toggle(mode === 'build') this.build_input.direction_arrow.toggle(mode === 'build') @@ -635,22 +726,24 @@ class Builder { // zoom this.zoom_container.toggle_pan_drag(mode === 'zoom' || mode === 'view') // resize canvas - this.map.canvas.toggle_resize(mode === 'zoom' || mode === 'brush') + this.map.canvas.toggle_resize(mode !== 'view') + // Behavior. Be careful of the order becuase rotation and - // toggle_selectable_drag both use Behavior.selectable_drag. + // toggle_selectable_drag both use Behavior.selectableDrag. if (mode === 'rotate') { - this.map.behavior.toggle_selectable_drag(false) // before toggle_rotation_mode - this.map.behavior.toggle_rotation_mode(true) + this.map.behavior.toggleSelectableDrag(false) // before toggle_rotation_mode + this.map.behavior.toggleRotationMode(true) // XX } else { - this.map.behavior.toggle_rotation_mode(mode === 'rotate') // before toggle_selectable_drag - this.map.behavior.toggle_selectable_drag(mode === 'brush') + this.map.behavior.toggleRotationMode(mode === 'rotate') // before toggleSelectableDrag + this.map.behavior.toggleSelectableDrag(mode === 'brush') // XX } - 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_label_touch(true) - this.map.behavior.toggle_text_label_edit(mode === 'text') - this.map.behavior.toggle_bezier_drag(mode === 'brush') + this.map.behavior.toggleSelectableClick(mode === 'build' || mode === 'brush') // XX + this.map.behavior.toggleLabelDrag(mode === 'brush') // XX + // this.map.behavior.toggleLabelMouseover(true) + // this.map.behavior.toggleLabelTouch(true) + this.map.behavior.toggleTextLabelEdit(mode === 'text') // XX + this.map.behavior.toggleBezierDrag(mode === 'brush') // XX + // edit selections if (mode === 'view' || mode === 'text') { this.map.select_none() @@ -658,134 +751,144 @@ class Builder { if (mode === 'rotate') { this.map.deselect_text_labels() } + this.map.draw_everything() + // what's not allowing me to delete this? XX above + + // callback + this.callback_manager.run('set_mode', null, mode) } - view_mode () { - /** For documentation of this function, see docs/javascript_api.rst. */ + /** For documentation of this function, see docs/javascript_api.rst. */ + view_mode () { // eslint-disable-line camelcase this.callback_manager.run('view_mode') - this._set_mode('view') + this._setMode('view') } - build_mode () { - /** For documentation of this function, see docs/javascript_api.rst. */ + /** For documentation of this function, see docs/javascript_api.rst. */ + build_mode () { // eslint-disable-line camelcase this.callback_manager.run('build_mode') - this._set_mode('build') + this._setMode('build') } - brush_mode () { - /** For documentation of this function, see docs/javascript_api.rst. */ + /** For documentation of this function, see docs/javascript_api.rst. */ + brush_mode () { // eslint-disable-line camelcase this.callback_manager.run('brush_mode') - this._set_mode('brush') + this._setMode('brush') } - zoom_mode () { - /** For documentation of this function, see docs/javascript_api.rst. */ + /** For documentation of this function, see docs/javascript_api.rst. */ + zoom_mode () { // eslint-disable-line camelcase this.callback_manager.run('zoom_mode') - this._set_mode('zoom') + this._setMode('zoom') } - rotate_mode () { - /** For documentation of this function, see docs/javascript_api.rst. */ + /** For documentation of this function, see docs/javascript_api.rst. */ + rotate_mode () { // eslint-disable-line camelcase this.callback_manager.run('rotate_mode') - this._set_mode('rotate') + this._setMode('rotate') } - text_mode () { - /** For documentation of this function, see docs/javascript_api.rst. */ + /** For documentation of this function, see docs/javascript_api.rst. */ + text_mode () { // eslint-disable-line camelcase this.callback_manager.run('text_mode') - this._set_mode('text') + this._setMode('text') } - _reaction_check_add_abs () { - const curr_style = this.options.reaction_styles - const 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 () { + _reactionCheckAddAbs () { + const currStyle = this.settings.get('reaction_styles') + if ( + this.settings.get('reaction_data') !== null && + !this.has_custom_reaction_styles && + !_.contains(currStyle, 'abs') + ) { + this.settings.set('reaction_styles', currStyle.concat('abs')) + return () => { this.map.set_status('Visualizing absolute value of reaction data. ' + 'Change this option in Settings.', 5000) - }.bind(this) + } } return null } /** - * Function to get props for the tooltip component - * @param {Object} props - Props that the tooltip will use - */ - pass_tooltip_component_props (props) { - this.tooltip_container.callback_manager.run('setState', null, props) - } - - /** - * Function to get props for the settings menu - * @param {Object} props - Props that the settings menu will use + * For documentation of this function, see docs/javascript_api.rst. */ - pass_settings_menu_props (props) { - // if (this.settings_menu.visible) { // <- pseudocode - // pass the props - // } - this.callback_manager.run('setState', null, props) + set_reaction_data (data) { // eslint-disable-line camelcase + this.settings.set('reaction_data', data) + + var messageFn = this._reactionCheckAddAbs() + + this._updateData(true, true, 'reaction') + + if (messageFn) messageFn() + else this.map.set_status('') + + const disabledButtons = this.settings.get('disabled_buttons') || [] + const buttonName = 'Clear reaction data' + const index = disabledButtons.indexOf(buttonName) + if (data !== null && index !== -1) { + this.settings.set('disabled_buttons', [ + ...disabledButtons.slice(0, index), + ...disabledButtons.slice(index + 1) + ]) + } else if (data === null && index === -1) { + this.settings.set('disabled_buttons', [...disabledButtons, buttonName]) + } } /** * For documentation of this function, see docs/javascript_api.rst. */ - set_reaction_data (data) { - this.options.reaction_data = data - var message_fn = this._reaction_check_add_abs() - if (message_fn !== null) { - this._update_data(true, true, 'reaction') - message_fn() - } else { - this.map.set_status('') + set_gene_data (data, clearGeneReactionRules) { // eslint-disable-line camelcase + if (clearGeneReactionRules) { + // default undefined + this.settings.set('show_gene_reaction_rules', false) } + this.settings.set('gene_data', data) + this._updateData(true, true, 'reaction') + this.map.set_status('') - let index = this.options.disabled_buttons.indexOf('Clear reaction data') + const disabledButtonsArray = this.settings.get('disabled_buttons') || [] + const index = disabledButtonsArray.indexOf('Clear gene data') if (index > -1) { - this.options.disabled_buttons.splice(index, 1) + disabledButtonsArray.splice(index, 1) + this.settings.set('disabled_buttons', disabledButtonsArray) } else if (index === -1 && data === null) { - this.options.disabled_buttons.push('Clear reaction data') + this.settings.set('disabled_buttons', [...disabledButtonsArray, 'Clear gene data']) } } /** * For documentation of this function, see docs/javascript_api.rst. */ - 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') + set_metabolite_data (data) { // eslint-disable-line camelcase + this.settings.set('metabolite_data', data) + this._updateData(true, true, 'metabolite') this.map.set_status('') - let index = this.options.disabled_buttons.indexOf('Clear gene data') + const disabledButtonsArray = this.settings.get('disabled_buttons') || [] + const index = disabledButtonsArray.indexOf('Clear metabolite data') if (index > -1) { - this.options.disabled_buttons.splice(index, 1) + disabledButtonsArray.splice(index, 1) + this.settings.set('disabled_buttons', disabledButtonsArray) } else if (index === -1 && data === null) { - this.options.disabled_buttons.push('Clear gene data') + this.settings.set('disabled_buttons', [...disabledButtonsArray, 'Clear metabolite data']) } } - 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('') - - let index = this.options.disabled_buttons.indexOf('Clear metabolite data') - if (index > -1) { - this.options.disabled_buttons.splice(index, 1) - } else if (index === -1 && data === null) { - this.options.disabled_buttons.push('Clear metabolite data') + _makeGeneDataObject (geneData, cobraModel, map) { + const allReactions = {} + if (cobraModel !== null) { + utils.extend(allReactions, cobraModel.reactions) } + // extend, overwrite + if (map !== null) { + utils.extend(allReactions, map.reactions, true) + } + + // this object has reaction keys and values containing associated genes + return dataStyles.import_and_check(geneData, 'gene_data', allReactions) } /** @@ -797,55 +900,52 @@ class Builder { * should_draw: (Optional, Default: true) Whether to redraw the update sections * of the map. */ - _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 + _updateData ( + updateModel = false, + updateMap = false, + kind = ['reaction', 'metabolite'], + shouldDraw = true + ) { + const updateReactionData = _.contains(kind, 'reaction') + const updateMetaboliteData = _.contains(kind, 'metabolite') + let metaboliteDataObject + let reactionDataObject + let geneDataObject // ------------------- // First map, and draw // ------------------- // metabolite data - if (update_metabolite_data && update_map && this.map !== null) { - met_data_object = dataStyles.import_and_check(this.options.metabolite_data, - 'metabolite_data') - this.map.apply_metabolite_data_to_map(met_data_object) - if (should_draw) { + if (updateMetaboliteData && updateMap && this.map !== null) { + metaboliteDataObject = dataStyles.import_and_check(this.settings.get('metabolite_data'), + 'metabolite_data') + this.map.apply_metabolite_data_to_map(metaboliteDataObject) + if (shouldDraw) { 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 = dataStyles.import_and_check(this.options.reaction_data, - 'reaction_data') - this.map.apply_reaction_data_to_map(reaction_data_object) - if (should_draw) { + if (updateReactionData) { + if (this.settings.get('reaction_data') !== null && updateMap && this.map !== null) { + reactionDataObject = dataStyles.import_and_check(this.settings.get('reaction_data'), + 'reaction_data') + this.map.apply_reaction_data_to_map(reactionDataObject) + if (shouldDraw) { 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) { + } else if (this.settings.get('gene_data') !== null && updateMap && this.map !== null) { + geneDataObject = this._makeGeneDataObject(this.settings.get('gene_data'), + this.cobra_model, this.map) + this.map.apply_gene_data_to_map(geneDataObject) + if (shouldDraw) { this.map.draw_all_reactions(false, false) } - } else if (update_map && this.map !== null) { + } else if (updateMap && this.map !== null) { // clear the data this.map.apply_reaction_data_to_map(null) - if (should_draw) { + if (shouldDraw) { this.map.draw_all_reactions(false, false) } } @@ -862,71 +962,55 @@ class Builder { } var delay = 5 - this.update_model_timer = setTimeout(function () { - + this.update_model_timer = setTimeout(() => { // metabolite_data - if (update_metabolite_data && update_model && this.cobra_model !== null) { + if (updateMetaboliteData && updateModel && this.cobra_model !== null) { // if we haven't already made this - if (!met_data_object) { - met_data_object = dataStyles.import_and_check(this.options.metabolite_data, - 'metabolite_data') + if (!metaboliteDataObject) { + metaboliteDataObject = dataStyles.import_and_check(this.settings.get('metabolite_data'), + 'metabolite_data') } - this.cobra_model.apply_metabolite_data(met_data_object, - this.options.metabolite_styles, - this.options.metabolite_compare_style) + this.cobra_model.apply_metabolite_data(metaboliteDataObject, + this.settings.get('metabolite_styles'), + this.settings.get('metabolite_compare_style')) } // reaction data - if (update_reaction_data) { - if (this.options.reaction_data !== null && update_model && this.cobra_model !== null) { + if (updateReactionData) { + if (this.settings.get('reaction_data') !== null && updateModel && this.cobra_model !== null) { // if we haven't already made this - if (!reaction_data_object) { - reaction_data_object = dataStyles.import_and_check(this.options.reaction_data, - 'reaction_data') + if (!reactionDataObject) { + reactionDataObject = dataStyles.import_and_check(this.settings.get('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_reaction_data(reactionDataObject, + this.settings.get('reaction_styles'), + this.settings.get('reaction_compare_style')) + } else if (this.settings.get('gene_data') !== null && updateModel && this.cobra_model !== null) { + if (!geneDataObject) { + geneDataObject = this._makeGeneDataObject(this.settings.get('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) { + this.cobra_model.apply_gene_data(geneDataObject, + this.settings.get('reaction_styles'), + this.settings.get('identifiers_on_map'), + this.settings.get('reaction_compare_style'), + this.settings.get('and_method_in_gene_reaction_rule')) + } else if (updateModel && this.cobra_model !== null) { // clear the data this.cobra_model.apply_reaction_data(null, - this.options.reaction_styles, - this.options.reaction_compare_style) + this.settings.get('reaction_styles'), + this.settings.get('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 dataStyles.import_and_check(gene_data, 'gene_data', all_reactions) - } + this.callback_manager.run('update_data', null, updateModel, updateMap, + kind, shouldDraw) + }, delay) } - _create_status (selection) { + _createStatus (selection) { this.status_bar = selection.append('div').attr('id', 'status') } @@ -937,7 +1021,7 @@ class Builder { _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 (this.settings.get('enable_editing') && !this.settings.get('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) @@ -966,37 +1050,13 @@ class Builder { this.quick_jump = QuickJump(selection, load_fn) } - _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 - was_enabled.label_touch = map.behavior.label_touch !== null - map.behavior.toggle_label_mouseover(false) - map.behavior.toggle_label_touch(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) - map.behavior.toggle_label_touch(was_enabled.label_touch) - was_enabled = {} - }) - } - /** * Define keyboard shortcuts */ - _get_keys (map, zoom_container, search_bar, settings_bar, - enable_editing, full_screen_button) { - var keys = { + getKeys () { + const map = this.map + const zoomContainer = this.zoom_container + let keys = { save: { key: 'ctrl+s', target: map, @@ -1048,24 +1108,24 @@ class Builder { }, zoom_in_ctrl: { key: 'ctrl+=', - target: zoom_container, - fn: zoom_container.zoom_in + target: zoomContainer, + fn: zoomContainer.zoom_in }, zoom_in: { key: '=', - target: zoom_container, - fn: zoom_container.zoom_in, + target: zoomContainer, + fn: zoomContainer.zoom_in, ignore_with_input: true }, zoom_out_ctrl: { key: 'ctrl+-', - target: zoom_container, - fn: zoom_container.zoom_out + target: zoomContainer, + fn: zoomContainer.zoom_out }, zoom_out: { key: '-', - target: zoom_container, - fn: zoom_container.zoom_out, + target: zoomContainer, + fn: zoomContainer.zoom_out, ignore_with_input: true }, extent_nodes_ctrl: { @@ -1090,15 +1150,6 @@ class Builder { fn: map.zoom_extent_canvas, ignore_with_input: true }, - search_ctrl: { - key: 'ctrl+f', - fn: () => this.renderSearchBar() - }, - search: { - key: 'f', - fn: () => this.renderSearchBar(), - ignore_with_input: true - }, view_mode: { target: this, fn: this.view_mode, @@ -1106,27 +1157,15 @@ class Builder { }, show_settings_ctrl: { key: 'ctrl+,', - target: settings_bar, - fn: () => this.pass_settings_menu_props({ - ...this.options, - map: this.map, - settings: this.settings, - display: true - }) + fn: () => this.passPropsSettingsMenu({ display: true }) }, show_settings: { key: ',', - target: this, - fn: () => this.pass_settings_menu_props({ - ...this.options, - map: this.map, - settings: this.settings, - display: true - }), + fn: () => this.passPropsSettingsMenu({ display: true }), ignore_with_input: true } } - if (full_screen_button) { + if (this.settings.get('full_screen_button')) { utils.extend(keys, { full_screen_ctrl: { key: 'ctrl+2', @@ -1141,7 +1180,7 @@ class Builder { } }) } - if (enable_editing) { + if (this.settings.get('enable_editing')) { utils.extend(keys, { build_mode: { key: 'n', @@ -1246,12 +1285,14 @@ class Builder { select_all: { key: 'ctrl+a', target: map, - fn: map.select_all + fn: map.select_all, + ignore_with_input: true }, select_none: { key: 'ctrl+shift+a', target: map, - fn: map.select_none + fn: map.select_none, + ignore_with_input: true }, invert_selection: { target: map, @@ -1259,6 +1300,19 @@ class Builder { } }) } + if (this.settings.get('enable_search')) { + utils.extend(keys, { + search_ctrl: { + key: 'ctrl+f', + fn: () => this.passPropsSearchBar({ display: true }) + }, + search: { + key: 'f', + fn: () => this.passPropsSearchBar({ display: true }), + ignore_with_input: true + } + }) + } return keys } @@ -1269,7 +1323,7 @@ class Builder { window.onbeforeunload = function (e) { // If we haven't been passed the event get the window.event e = e || window.event - return (this.options.never_ask_before_quit + return (this.settings.get('never_ask_before_quit') ? null : 'You will lose any unsaved changes.' ) diff --git a/src/BuilderMenuBar.jsx b/src/BuilderMenuBar.jsx deleted file mode 100644 index 9bea754b..00000000 --- a/src/BuilderMenuBar.jsx +++ /dev/null @@ -1,243 +0,0 @@ -/** @jsx h */ -import { h, Component } from 'preact' -import Dropdown from './Dropdown' -import MenuButton from './MenuButton' - -/** - * BuilderMenuBar. Wrapper class that implements generic Dropdown and MenuButton - * objects to create the Builder menu bar. Currently re-renders every time an - * edit mode is chosen. This can be changed once Builder is ported to Preact. - */ -class BuilderMenuBar extends Component { - - componentDidMount () { - this.props.sel.selectAll('#canvas').on( - 'touchend', () => this.setState({visible: false}) - ) - this.props.sel.selectAll('#canvas').on( - 'click', () => this.setState({visible: false}) - ) - } - - render () { - return ( -
    - - this.props.saveMap()} - disabledButtons={this.props.disabled_buttons} - /> - this.props.loadMap(file)} - type='load' - disabledButtons={this.props.disabled_buttons} - /> - this.props.saveSvg()} - disabledButtons={this.props.disabled_buttons} - /> - this.props.savePng()} - disabledButtons={this.props.disabled_buttons} - /> - this.props.clearMap()} - disabledButtons={this.props.disabled_buttons} - /> - - - this.props.loadModel(file)} - type='load' - disabledButtons={this.props.disabled_buttons} - /> - this.props.updateRules()} - disabledButtons={this.props.disabled_buttons} - /> - this.props.loadModel(null)} - disabledButtons={this.props.disabled_buttons} - /> - - - this.props.loadReactionData(file)} - type='load' - disabledButtons={this.props.disabled_buttons} - /> - this.props.loadReactionData(null)} - disabledButtons={this.props.disabled_buttons} - /> -
  • - this.props.loadGeneData(file)} - type='load' - disabledButtons={this.props.disabled_buttons} - /> - this.props.loadGeneData(null)} - disabledButtons={this.props.disabled_buttons} - /> -
  • - this.props.loadMetaboliteData(file)} - type='load' - disabledButtons={this.props.disabled_buttons} - /> - this.props.loadMetaboliteData(null)} - disabledButtons={this.props.disabled_buttons} - /> - - - this.props.setMode('zoom')} - disabledButtons={this.props.disabled_buttons} - /> - this.props.setMode('brush')} - disabledButtons={this.props.disabled_buttons} - /> - this.props.setMode('build')} - disabledButtons={this.props.disabled_buttons} - /> - this.props.setMode('rotate')} - disabledButtons={this.props.disabled_buttons} - /> - this.props.setMode('text')} - disabledButtons={this.props.disabled_buttons} - /> -
  • - this.props.deleteSelected()} - disabledButtons={this.props.disabled_buttons} - /> - this.props.undo()} - disabledButtons={this.props.disabled_buttons} - /> - this.props.redo()} - disabledButtons={this.props.disabled_buttons} - /> - this.props.togglePrimary()} - disabledButtons={this.props.disabled_buttons} - /> - this.props.cyclePrimary()} - disabledButtons={this.props.disabled_buttons} - /> - this.props.selectAll()} - disabledButtons={this.props.disabled_buttons} - /> - this.props.selectNone()} - disabledButtons={this.props.disabled_buttons} - /> - this.props.invertSelection()} - disabledButtons={this.props.disabled_buttons} - /> - - - this.props.zoomIn()} - disabledButtons={this.props.disabled_buttons} - /> - this.props.zoomOut()} - disabledButtons={this.props.disabled_buttons} - /> - this.props.zoomExtentNodes()} - disabledButtons={this.props.disabled_buttons} - /> - this.props.zoomExtentCanvas()} - disabledButtons={this.props.disabled_buttons} - /> - this.props.search()} - disabledButtons={this.props.disabled_buttons} - /> - { - this.props.toggleBeziers() - this.props.setMode(this.props.mode) - } - } - disabledButtons={this.props.disabled_buttons} - /> -
  • - this.props.renderSettingsMenu()} - disabledButtons={this.props.disabled_buttons} - type='settings' - /> - - ? -
- ) - } -} - -export default BuilderMenuBar diff --git a/src/ButtonPanel.css b/src/ButtonPanel.css index 01c540b1..772ce159 100644 --- a/src/ButtonPanel.css +++ b/src/ButtonPanel.css @@ -1,18 +1,13 @@ -.escher-container .buttonPanel * { - box-sizing: border-box; -} - -.escher-container .buttonPanel { +.escher-container .button-panel { position: absolute; left: 4px; top: 20%; margin-top: -32px; padding-left: 0; - -ms-touch-action: none; touch-action: none; } -.escher-container .buttonPanel>li { +.escher-container .button-panel>li { margin-top: 5px; } @@ -37,7 +32,7 @@ border-bottom-right-radius: 4px; } -.escher-container .buttonPanel>.grouping:last-child { +.escher-container .button-panel>.grouping:last-child { margin-top: 4px; } @@ -68,15 +63,15 @@ height: 40px; } -.escher-container .buttonPanel .button:active, .escher-container .buttonGroup label:active, .escher-container .buttonPanel .buttonGroup:active { +.escher-container .button-panel .button:active, .escher-container .buttonGroup label:active, .escher-container .button-panel .buttonGroup:active { background-image: linear-gradient(#3F4141, #474949 6%, #4F5151); } -.escher-container .buttonPanel .fa { +.escher-container .button-panel .fa { font-size: 24px; } /* Icons */ -.escher-container .buttonPanel [class^='icon-'] { +.escher-container .button-panel [class^='icon-'] { font-size: 23px; } diff --git a/src/ButtonPanel.jsx b/src/ButtonPanel.jsx index 47179dde..5cd08498 100644 --- a/src/ButtonPanel.jsx +++ b/src/ButtonPanel.jsx @@ -9,13 +9,17 @@ import './ButtonPanel.css' */ class ButtonPanel extends Component { render () { + const menuSetting = this.props.settings.get('menu') + const enableKeys = this.props.settings.get('enable_keys') + const enableEditing = this.props.settings.get('enable_editing') + return ( -
    +
    • @@ -24,46 +28,36 @@ class ButtonPanel extends Component {
    • -
    • +
    • -
    • +
    • - -
      - {this.state.counter} -
      +
      + this.handleInput(event.target.value)} + ref={input => { this.inputRef = input }} + /> + + +
      + {this.state.counter}
      -
      ) diff --git a/src/Settings.js b/src/Settings.js index d932f440..8199f6b5 100644 --- a/src/Settings.js +++ b/src/Settings.js @@ -1,58 +1,9 @@ -/** Settings. A class to manage settings for a Map. - - Arguments - --------- - - setOption: A function, fn(key), that returns the option value for the - key. - - getOption: A function, fn(key, value), that sets the option for the key - and value. - - conditionalOptions: The options to that are conditionally accepted when - changed. Changes can be abandoned by calling abandon_changes(), or accepted - by calling accept_changes(). - - */ - -import utils from './utils' import bacon from 'baconjs' - -var Settings = utils.make_class() -// instance methods -Settings.prototype = { - init, - set_conditional, - hold_changes, - abandon_changes, - accept_changes, - createConditionalSetting, - convertToConditionalStream -} -module.exports = Settings - -function init (setOption, getOption, conditionalOptions) { - this.set_option = setOption - this.get_option = getOption - - // Manage accepting/abandoning changes - this.status_bus = new bacon.Bus() - - // Create the options - this.busses = {} - this.streams = {} - for (var i = 0, l = conditionalOptions.length; i < l; i++) { - var name = conditionalOptions[i] - var out = createConditionalSetting(name, getOption(name), setOption, - this.status_bus) - this.busses[name] = out.bus - this.streams[name] = out.stream - } -} +import _ from 'underscore' /** - * Hold on to event when hold_property is true, and only keep them if - * accept_property is true (when hold_property becomes false). + * Hold on to event when holdProperty is true, and only keep them if + * acceptProperty is true (when holdProperty becomes false). */ function convertToConditionalStream (valueStream, statusStream) { // Combine values with status to revert to last value when a reject is passed. @@ -104,61 +55,128 @@ function convertToConditionalStream (valueStream, statusStream) { } } }) - // Skip the initial null value + // Skip the initial null value .skip(1) - // Get the current value + // Get the current value .map(({ currentValue }) => currentValue) - // Skip duplicate values + // Skip duplicate values .skipDuplicates() - // property -> event stream + // property -> event stream .toEventStream() return held } -function createConditionalSetting (name, initialValue, setOption, statusBus) { - // Set up the bus - var bus = new bacon.Bus() +/** + * Settings. A class to manage settings for a Map. + * + * Arguments + * --------- + * + * setOption: A function, fn(key), that returns the option value for the key. + * + * getOption: A function, fn(key, value), that sets the option for the key and + * value. + * + * conditionalOptions: The options to that are conditionally accepted when + * changed. Changes can be abandoned by calling abandonChanges(), or accepted + * by calling acceptChanges(). + * @param optionsWithDefaults - The current option values + * @param conditionalOptions - The options to that are conditionally accepted + * when changed. Changes can be abandoned by calling + * abandon_changes(), or accepted by calling + * accept_changes(). + */ +export default class Settings { + constructor (optionsWithDefaults, conditionalOptions) { + this._options = optionsWithDefaults + + // Manage accepting/abandoning changes + this.statusBus = new bacon.Bus() + + // Create the options + ;[ this.busses, this.streams ] = _.chain(optionsWithDefaults) + .mapObject((value, key) => { + const isConditional = _.contains(conditionalOptions, key) + const { bus, stream } = this.createSetting(key, value, isConditional) + return [ bus, stream ] + }) // { k: [ b, s ], ... } + .pairs() // [ [ k, [ b, s ] ], ... ] + .map(([ name, [ bus, stream ] ]) => [[ name, bus ], [ name, stream ]]) + // [ [ [ k, b ], [ k, s ] ], ... ] + .unzip() // [ [ [ k, b ], ... ], [ [ k, s ], ... ] ] + .map(x => _.object(x)) // [ { k: b, ... }, { k: s, ... } ] + .value() + } - // Make the event stream and conditionally accept changes - var stream = convertToConditionalStream(bus, statusBus) + /** + * Set up a new bus and stream for a conditional setting (i.e. one that can be + * canceledin the settings menu. + */ + createSetting (name, initialValue, isConditional) { + // Set up the bus + const bus = new bacon.Bus() - // Get the latest - stream.onValue(v => setOption(name, v)) + // Make the event stream and conditionally accept changes + const stream = isConditional + ? convertToConditionalStream(bus, this.statusBus) + : bus.toEventStream() - // Push the initial value - bus.push(initialValue) + // Get the latest + stream.onValue(v => { this._options[name] = v }) - return { bus, stream } -} + // Push the initial value + bus.push(initialValue) -/** Set the value of a conditional setting, one that will only be - accepted if this.accept_changes() is called. + return { bus, stream } + } - Arguments - --------- + /** + * Deprecated. Use `set` instead. + */ + set_conditional (name, value) { // eslint-disable-line camelcase + console.warn('set_conditional is deprecated. Use Settings.set() instead') + return this.set(name, value) + } - name: The option name + /** + * Set the option. This should always be used instead of setting options + * directly. To set options that respect the Settings menu Accept/Abandon, use + * setConditional(). + * @param {String} name - The option name + * @param {} value - The new value + */ + set (name, value) { + if (!(name in this.busses)) { + throw new Error(`Invalid setting name ${name}`) + } + this.busses[name].push(value) + } - value: The new value + /** + * Deprecated. Use `get` intead. + */ + get_option (name) { // eslint-disable-line camelcase + console.warn('get_option is deprecated. Use Settings.get() instead') + return this.get(name) + } - */ -function set_conditional (name, value) { - if (!(name in this.busses)) { - console.error(`Invalid setting name ${name}`) - } else { - this.busses[name].push(value) + /** + * Get an option + */ + get (name) { + return this._options[name] } -} -function hold_changes () { - this.status_bus.push('hold') -} + holdChanges () { + this.statusBus.push('hold') + } -function abandon_changes () { - this.status_bus.push('abandon') -} + abandonChanges () { + this.statusBus.push('abandon') + } -function accept_changes() { - this.status_bus.push('accept') + acceptChanges () { + this.statusBus.push('accept') + } } diff --git a/src/BuilderSettingsMenu.css b/src/SettingsMenu.css similarity index 100% rename from src/BuilderSettingsMenu.css rename to src/SettingsMenu.css diff --git a/src/BuilderSettingsMenu.jsx b/src/SettingsMenu.jsx similarity index 62% rename from src/BuilderSettingsMenu.jsx rename to src/SettingsMenu.jsx index 5740f392..3d480bac 100644 --- a/src/BuilderSettingsMenu.jsx +++ b/src/SettingsMenu.jsx @@ -3,9 +3,8 @@ import { h, Component } from 'preact' import ScaleSelector from './ScaleSelector' import ScaleSlider from './ScaleSlider' import ScaleSelection from './ScaleSelection' -import update from 'immutability-helper' import _ from 'underscore' -import './BuilderSettingsMenu.css' +import './SettingsMenu.css' import scalePresets from './colorPresets' /** @@ -13,29 +12,9 @@ import scalePresets from './colorPresets' * settings. Implements Settings.js but otherwise only uses * Preact. */ -class BuilderSettingsMenu extends Component { - constructor (props) { - super(props) - this.state = { - display: props.display - } - if (props.display) { - this.componentWillAppear() - } - } - - componentWillReceiveProps (nextProps) { - this.setState({display: nextProps.display}) - if (nextProps.display && !this.props.display) { - this.componentWillAppear() - } - if (!nextProps.display && this.props.display) { - this.componentWillDisappear() - } - } - - componentWillAppear () { - this.props.settings.hold_changes() +class SettingsMenu extends Component { + componentWillMount () { + this.props.settings.holdChanges() this.setState({ clearEscape: this.props.map.key_manager.add_escape_listener( () => this.abandonChanges(), @@ -49,49 +28,46 @@ class BuilderSettingsMenu extends Component { }) } - componentWillDisappear () { - this.props.closeMenu() // Function to pass display = false to the settings menu + componentWillUnmount () { this.state.clearEscape() this.state.clearEnter() } abandonChanges () { - this.props.settings.abandon_changes() - this.componentWillDisappear() + this.props.settings.abandonChanges() + this.props.setDisplay(false) } saveChanges () { - this.props.settings.accept_changes() - this.componentWillDisappear() + this.props.settings.acceptChanges() + this.props.setDisplay(false) } /** - * Function to handle changes to the reaction or metabolite styling. + * Function to toggle one option in the reaction or metabolite styling. * @param {String} value - the style option to be added or removed * @param {String} type - reaction_style or metabolite_style */ handleStyle (value, type) { - if (this.props[type].indexOf(value) === -1) { - this.props.settings.set_conditional(type, - update(this.props[type], {$push: [value]}) - ) - } else if (this.props[type].indexOf(value) > -1) { - this.props.settings.set_conditional(type, - update(this.props[type], {$splice: [[this.props[type].indexOf(value), 1]]}) - ) + const currentSetting = this.props.settings.get(type) + const index = currentSetting.indexOf(value) + if (index === -1) { + this.props.settings.set(type, [...currentSetting, value]) + } else { + this.props.settings.set(type, [ + ...currentSetting.slice(0, index), + ...currentSetting.slice(index + 1) + ]) } } - is_visible () { - return this.state.display - } - render () { + const settings = this.props.settings + const enableTooltips = settings.get('enable_tooltips') || [] + const dataStatistics = this.props.map.get_data_statistics() + return ( -
      +