diff --git a/.gitmodules b/.gitmodules
index 05f46a77aa..1e36ca9171 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -37,3 +37,6 @@
[submodule "anvio/data/interactive/lib/JavaScript-MD5"]
path = anvio/data/interactive/lib/JavaScript-MD5
url = https://github.com/blueimp/JavaScript-MD5.git
+[submodule "anvio/data/interactive/lib/fabric.js"]
+ path = anvio/data/interactive/lib/fabric
+ url = https://github.com/fabricjs/fabric.js.git
diff --git a/anvio/__init__.py b/anvio/__init__.py
index 7665eabfbe..fe7a55d76b 100644
--- a/anvio/__init__.py
+++ b/anvio/__init__.py
@@ -166,6 +166,12 @@ def TABULATE(table, header, numalign="right", max_width=0):
'required': True,
'help': "Anvi'o structure database."}
),
+ 'genome-view-db': (
+ ['-E', '--genome-view-db'],
+ {'metavar': "GENOME_VIEW_DB",
+ 'required': True,
+ 'help': "Anvi'o genome view database."}
+ ),
'only-if-structure': (
['--only-if-structure'],
{'default': False,
@@ -3563,7 +3569,8 @@ def set_version():
t.genomes_storage_vesion, \
t.structure_db_version, \
t.metabolic_modules_db_version, \
- t.trnaseq_db_version
+ t.trnaseq_db_version, \
+ t.genome_view_db_version
def get_version_tuples():
@@ -3576,7 +3583,8 @@ def get_version_tuples():
("Genome data storage version", __genomes_storage_version__),
("Structure DB version", __structure__version__),
("KEGG Modules DB version", __kegg_modules_version__),
- ("tRNA-seq DB version", __trnaseq__version__)]
+ ("tRNA-seq DB version", __trnaseq__version__),
+ ("Genome view DB version", __genome_view_db__version__)]
def print_version():
@@ -3588,7 +3596,8 @@ def print_version():
run.info("Auxiliary data storage", __auxiliary_data_version__)
run.info("Structure database", __structure__version__)
run.info("Metabolic modules database", __kegg_modules_version__)
- run.info("tRNA-seq database", __trnaseq__version__, nl_after=1)
+ run.info("tRNA-seq database", __trnaseq__version__)
+ run.info("Genome view database", __genome_view_db__version__, nl_after=1)
__version__, \
@@ -3601,7 +3610,8 @@ def print_version():
__genomes_storage_version__ , \
__structure__version__, \
__kegg_modules_version__, \
-__trnaseq__version__ = set_version()
+__trnaseq__version__, \
+__genome_view_db__version__ = set_version()
if '-v' in sys.argv or '--version' in sys.argv:
diff --git a/anvio/bottleroutes.py b/anvio/bottleroutes.py
index 75ee0c4f7d..d976840966 100644
--- a/anvio/bottleroutes.py
+++ b/anvio/bottleroutes.py
@@ -188,6 +188,8 @@ def register_routes(self):
self.route('/data/reroot_tree', callback=self.reroot_tree, method='POST')
self.route('/data/save_tree', callback=self.save_tree, method='POST')
self.route('/data/check_homogeneity_info', callback=self.check_homogeneity_info, method='POST')
+ self.route('/data/get_genome_view_data', callback=self.get_genome_view_data, method='POST')
+ self.route('/data/get_genome_view_adl', callback=self.get_genome_view_continuous_data_layers, method='POST')
self.route('/data/search_items', callback=self.search_items_by_name, method='POST')
self.route('/data/get_taxonomy', callback=self.get_taxonomy, method='POST')
self.route('/data/get_functions_for_gene_clusters', callback=self.get_functions_for_gene_clusters, method='POST')
@@ -264,6 +266,8 @@ def redirect_to_app(self):
homepage = 'metabolism.html'
elif self.interactive.mode == 'inspect':
redirect('/app/charts.html?id=%s&show_snvs=true&rand=%s' % (self.interactive.inspect_split_name, self.random_hash(8)))
+ elif self.interactive.mode == 'genome-view':
+ homepage = 'genomeview.html'
redirect('/app/%s?rand=%s' % (homepage, self.random_hash(8)))
@@ -469,9 +473,13 @@ def save_state(self, state_name):
def get_state(self, state_name):
if state_name in self.interactive.states_table.states:
+
state = self.interactive.states_table.states[state_name]
state_dict = json.loads(state['content'])
+ if self.interactive.mode == 'genome-view':
+ return json.dumps({'content' : state_dict})
+
if self.interactive.mode == 'structure':
return json.dumps({'content': state['content']})
else:
@@ -1357,6 +1365,24 @@ def get_initial_data(self):
return json.dumps(self.interactive.get_initial_data())
+ def get_genome_view_data(self):
+ try:
+ return json.dumps({'genomes': self.interactive.genomes,
+ 'gene_associations': self.interactive.gene_associations})
+ except Exception as e:
+ return json.dumps({'error': f"Something went wrong at the backend :( Here is the error message: '{e}'"})
+
+
+ def get_genome_view_continuous_data_layers(self):
+ """populate continuous data layers, and send them back to the frontend"""
+
+ try:
+ self.interactive.populate_genome_continuous_data_layers()
+ return json.dumps(self.interactive.continuous_data_layers)
+ except Exception as e:
+ return json.dumps({'error': f"Something went wrong at the backend :( Here is the error message: '{e}'"})
+
+
def get_column_info(self):
gene_callers_id = int(request.forms.get('gene_callers_id'))
engine = request.forms.get('engine')
@@ -1477,7 +1503,7 @@ def get_functions_for_gene_clusters(self):
message = (f"At least one of the gene clusters in your list (e.g., {gene_cluster_name}) is missing in "
f"the functions summary dict :/")
return json.dumps({'status': 1, 'message': message})
-
+
d[gene_cluster_name] = self.interactive.gene_clusters_functions_summary_dict[gene_cluster_name]
return json.dumps({'functions': d, 'sources': list(self.interactive.gene_clusters_function_sources)})
diff --git a/anvio/data/interactive/css/genomeview.css b/anvio/data/interactive/css/genomeview.css
new file mode 100644
index 0000000000..50d070c489
--- /dev/null
+++ b/anvio/data/interactive/css/genomeview.css
@@ -0,0 +1,275 @@
+.container {
+ width: 95%;
+ align-content: center;
+ margin: 0 auto 20px auto;
+ background: rgba(255, 255, 255, 0.5);
+ box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.2);
+ padding: 20px 30px;
+ touch-action: none;
+}
+
+svg {
+ font-family: 'Lato', Arial, serif;
+ font-size: 10px;
+ font-weight: 700;
+ text-shadow: 0 1px 1px rgba(255, 255, 255, 0.8);
+}
+
+g.scale g.brush rect.background {
+ fill: rgba(0, 0, 0, .1);
+}
+
+g.scale g.axis path {
+ stroke-opacity: 0;
+}
+
+g.scale g.axis line {
+ stroke-opacity: .5;
+}
+
+.axis path,
+.axis line {
+ fill: none;
+ stroke: #aaa;
+ shape-rendering: crispEdges;
+}
+
+.brush .extent {
+ stroke: black;
+ fill-opacity: .125;
+ shape-rendering: crispEdges;
+}
+
+g.scale rect.background {
+ fill: rgb(200, 200, 255);
+ visibility: visible !important;
+ pointer-events: all;
+}
+
+#tooltip-body {
+ background-color: white;
+ opacity: 100%;
+ border-style: solid;
+ border-width: 3px;
+ padding: 10px;
+}
+
+#deepdive-tooltip-body {
+ background-color: white;
+ opacity: 100%;
+ border-style: solid;
+ border-width: 3px;
+ padding: 10px;
+}
+
+
+#toggle-panel-settings {
+ position: fixed;
+ height: 100px;
+ width: 15px;
+ background-color: #EEEEEE;
+ border: solid 1px #DDDDDD;
+ top: calc(40%);
+ border-radius: 10px 0px 0px 10px;
+ cursor: pointer;
+ font-size: 10px;
+ padding-top: 14px;
+ right: 0px;
+ z-index: 999;
+}
+
+#toggle-panel-query {
+ position: fixed;
+ height: 100px;
+ width: 15px;
+ background-color: #EEEEEE;
+ border: solid 1px #DDDDDD;
+ top: calc(55%);
+ border-radius: 10px 0px 0px 10px;
+ cursor: pointer;
+ font-size: 10px;
+ padding-top: 14px;
+ right: 0px;
+ z-index: 999;
+}
+
+#toggle-panel-mouseover {
+ position: fixed;
+ height: 100px;
+ width: 15px;
+ background-color: #EEEEEE;
+ border: solid 1px #DDDDDD;
+ top: calc(70%);
+ border-radius: 10px 0px 0px 10px;
+ cursor: pointer;
+ font-size: 10px;
+ padding-top: 14px;
+ right: 0px;
+ z-index: 999;
+}
+
+#settings-panel {
+ display: none;
+ top: 0px;
+ right: 0px;
+ position: fixed;
+ height: 100%;
+ background: url('../images/fractal.jpg') center center scroll;
+ border-left: 1px solid #D3D3D3;
+ opacity: 0.97;
+ width: 530px;
+ padding: 20px;
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ overflow-y: scroll;
+ z-index: 999;
+ color: black;
+}
+
+#query-panel {
+ display: none;
+ top: 0px;
+ right: 0px;
+ position: fixed;
+ height: 100%;
+ background: url('../images/fractal.jpg') center center scroll;
+ border-left: 1px solid #D3D3D3;
+ opacity: 0.97;
+ width: 530px;
+ padding: 20px;
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ overflow-y: scroll;
+ z-index: 999;
+ color: black;
+}
+
+#mouseover-panel {
+ display: none;
+ top: 0px;
+ right: 0px;
+ position: fixed;
+ height: 100%;
+ background: url('../images/fractal.jpg') center center scroll;
+ border-left: 1px solid #D3D3D3;
+ opacity: 0.97;
+ width: 530px;
+ padding: 20px;
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ overflow-y: scroll;
+ z-index: 999;
+ color: black;
+}
+
+.settings-header {
+ font-variant: small-caps;
+ font-size: 1.2em;
+ border-bottom: 1px #969696 solid;
+ display: block;
+ margin-bottom: 5px;
+ margin-top: 12px;
+}
+
+.settings-section-info {
+ background: #e7f3381a;
+ padding: 3px 6px 5px 10px;
+ border: 1px dashed #a9a9a9;
+ border-radius: 9px;
+ font-size: 12px;
+}
+
+#settings-section-info-SNV-warning {
+ display: none;
+ color: red;
+}
+
+#settings-panel code {
+ padding: 1.4px 2px;
+ font-size: 90%;
+ color: #c7254e;
+ background-color: #f3f3f3;
+ border-radius: 2px;
+}
+
+.settings-info-line {
+ text-align: center;
+ border: 1px dashed #c5c5c5;
+ background: #f3f3f3;
+ width: 40%;
+ margin: auto;
+ border-radius: 7px;
+ font-style: italic;
+}
+
+.toggle-panel-settings-pos {
+ right: 530px !important;
+}
+
+.toggle-panel-query-pos {
+ right: 530px !important;
+}
+
+.toggle-panel-mouseover-pos {
+ right: 530px !important;
+}
+
+#settings-panel .btn {
+ margin-bottom: 5px !important;
+}
+
+.sidebar-footer {
+ position: fixed;
+ background-color: white;
+ width: 420px;
+ bottom: -0px;
+ border-top: 1px solid #D3D3D3;
+ border-right: 1px solid #D3D3D3;
+}
+
+.sidebar-footer>.btn {
+ width: 50%;
+}
+
+body {
+ font-family: 'Lato', Arial, serif;
+ font-weight: 300;
+ font-size: 14px;
+ -webkit-font-smoothing: antialiased;
+}
+
+a {
+ color: #722;
+ text-decoration: none;
+}
+
+#labelAndMainContainer {
+ display: flex;
+ flex-direction: row;
+}
+
+#labelAndDropdownContainer {
+ border-style: solid;
+ border-width: 1;
+ padding: 15px;
+ margin: 5px;
+}
+
+#scaleContainer {
+ border-style: solid;
+ border-width: 3px;
+ background: white;
+ padding: 15px;
+ margin: 5px;
+ position: fixed;
+ bottom: 25px;
+ background-color: rgba(255, 255, 255, 0.8);
+}
+
+#tabular-modal-body {
+ overflow-y: scroll;
+}
+
+.modal-multiselect-col {
+ display: flex;
+ flex-direction: column;
+ width: 40%;
+ margin: 15px;
+}
\ No newline at end of file
diff --git a/anvio/data/interactive/genomeview.html b/anvio/data/interactive/genomeview.html
new file mode 100644
index 0000000000..54646f937b
--- /dev/null
+++ b/anvio/data/interactive/genomeview.html
@@ -0,0 +1,652 @@
+
+
+
+
+ Genome View
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Display Viewport
+ Data
+
-
+
+
+
+
+
+ Bookmarks
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Glow selected in
+ sequence
+ Export selected as contig.db
+ Export table as TSV
+
+
+
+
+ Add metadata tag to selected
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ◀ S ETTINGS
+
+
+
+
+
+
+
+ ◀ Q UERY
+
+
+
+
+
+
+
+ Gene ID
+ Genome
+ Start
+ Stop
+ Action
+
+
+
+
+
+
+
+
+
+ ◀ M OUSE
+
+
+
Gene Call
+
+
+ ID
+ Source
+ Length
+ Direction
+ Start
+ Stop
+ Call type
+
+
+
+
+
Annotations
+
+
+ Source
+ Accession
+ Annotation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Last modified: n/a
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/anvio/data/interactive/js/animations.js b/anvio/data/interactive/js/animations.js
index 16b99afbf3..e771bc2a12 100644
--- a/anvio/data/interactive/js/animations.js
+++ b/anvio/data/interactive/js/animations.js
@@ -1,3 +1,21 @@
+/**
+ * Functions for panel animations.
+ *
+ * Authors: Özcan Esen
+ *
+ * Copyright 2015-2021, The anvi'o project (http://anvio.org)
+ *
+ * Anvi'o is a free software. You can redistribute this program
+ * and/or modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation, either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with anvi'o. If not, see .
+ *
+ * @license GPL-3.0+
+ */
+
var ANIMATIONS_ENABLED = true;
var SLIDE_INTERVAL = 4;
var SLIDE_STEP_SIZE = 15;
@@ -11,7 +29,7 @@ function toggleLeftPanel() {
is_left_panel_sliding = true;
if ($('#panel-left').is(':visible')) {
- var animation_frame = function(){
+ var animation_frame = function(){
if (ANIMATIONS_ENABLED && $('#panel-left')[0].getBoundingClientRect().right > 0) {
$('#panel-left').css('left', parseInt($('#panel-left').css('left')) - SLIDE_STEP_SIZE);
$('#toggle-panel-left').css('left', $('#sidebar')[0].getBoundingClientRect().right + 'px');
@@ -28,7 +46,7 @@ function toggleLeftPanel() {
animation_frame();
} else {
$('#panel-left').show();
- var animation_frame = function(){
+ var animation_frame = function(){
if (ANIMATIONS_ENABLED && $('#panel-left')[0].getBoundingClientRect().left < 0) {
$('#panel-left').css('left', parseInt($('#panel-left').css('left')) + SLIDE_STEP_SIZE);
$('#toggle-panel-left').css('left', $('#sidebar')[0].getBoundingClientRect().right + 'px');
@@ -50,7 +68,7 @@ function toggleRightPanel(name) {
['#mouse_hover_panel', '#description-panel', '#news-panel'].forEach(function(right_panel) {
if (right_panel == name)
return;
-
+
$(right_panel).hide();
});
diff --git a/anvio/data/interactive/js/area-zoom.js b/anvio/data/interactive/js/area-zoom.js
index 24b97881a1..40d1805e97 100644
--- a/anvio/data/interactive/js/area-zoom.js
+++ b/anvio/data/interactive/js/area-zoom.js
@@ -1,3 +1,22 @@
+/**
+ * Zooming in an out.
+ *
+ * Authors: Özcan Esen
+ *
+ * Copyright 2015-2021, The anvi'o project (http://anvio.org)
+ *
+ * Anvi'o is a free software. You can redistribute this program
+ * and/or modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation, either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with anvi'o. If not, see .
+ *
+ * @license GPL-3.0+
+ */
+
+
var mouse_event_origin_x = 0;
var mouse_event_origin_y = 0;
@@ -9,8 +28,8 @@ function initialize_area_zoom() {
var viewport = document.getElementById('svg');
viewport.addEventListener('mousedown',
- function(event) {
- dragging = false;
+ function(event) {
+ dragging = false;
document.activeElement.blur();
mouse_event_origin_x = event.clientX;
@@ -28,11 +47,11 @@ function initialize_area_zoom() {
}
});
- viewport.addEventListener('mousemove',
+ viewport.addEventListener('mousemove',
function(event) {
if (Math.abs(mouse_event_origin_x - event.clientX) + Math.abs(mouse_event_origin_y - event.clientY) > 2)
{
- dragging = true;
+ dragging = true;
}
if (event.shiftKey && drawing_zoom)
@@ -56,25 +75,25 @@ function initialize_area_zoom() {
}
});
- viewport.addEventListener('mouseup',
+ viewport.addEventListener('mouseup',
function() {
if (drawing_zoom)
{
var zoom_rect = document.getElementById('divzoom').getBoundingClientRect();
-
+
if (zoom_rect.width > 2 && zoom_rect.height > 2)
{
var _dx = (parseInt("0" + $('#svg').position().left) + (VIEWER_WIDTH / 2)) - (zoom_rect.left + zoom_rect.width / 2);
var _dy = (parseInt("0" + $('#svg').position().top) + (VIEWER_HEIGHT / 2)) - (zoom_rect.top + zoom_rect.height / 2);
- pan(_dx,_dy);
+ pan(_dx,_dy);
zoom(Math.min(VIEWER_WIDTH / zoom_rect.width, VIEWER_HEIGHT / zoom_rect.height));
}
}
clearTextSelection();
- drawing_zoom=false;
- zoomBox = {};
- $('#divzoom').hide();
+ drawing_zoom=false;
+ zoomBox = {};
+ $('#divzoom').hide();
});
}
diff --git a/anvio/data/interactive/js/bin.js b/anvio/data/interactive/js/bin.js
index 207401be0d..63793ac59e 100644
--- a/anvio/data/interactive/js/bin.js
+++ b/anvio/data/interactive/js/bin.js
@@ -1,11 +1,11 @@
/**
* Draw bins, bin labels stuff.
*
- * Author: Özcan Esen
- * Credits: A. Murat Eren
- * Copyright 2017, The anvio Project
+ * Authors: Özcan Esen
+ * A. Murat Eren
*
- * This file is part of anvi'o ().
+ *
+ * Copyright 2015-2021, The anvi'o project (http://anvio.org)
*
* Anvi'o is a free software. You can redistribute this program
* and/or modify it under the terms of the GNU General Public
diff --git a/anvio/data/interactive/js/charts.js b/anvio/data/interactive/js/charts.js
index b1947117be..fd37cbf0ee 100644
--- a/anvio/data/interactive/js/charts.js
+++ b/anvio/data/interactive/js/charts.js
@@ -1,11 +1,14 @@
/**
* Javascript library to visualize anvi'o charts
*
- * Author: A. Murat Eren
- * Credits: Özcan Esen, Gökmen Göksel, Tobias Paczian.
- * Copyright 2015, The anvio Project
+ * Authors: A. Murat Eren
+ * Ozcan Esen
+ * Isaac Fink
+ * Matthew Klein
+ * Gökmen Göksel
+ * Tobias Paczian
*
- * This file is part of anvi'o ().
+ * Copyright 2015-2021, The anvi'o project (http://anvio.org)
*
* Anvi'o is a free software. You can redistribute this program
* and/or modify it under the terms of the GNU General Public
@@ -51,10 +54,8 @@ var gene_offset_y = 0;
var select_boxes = {};
var curr_height;
var show_cags_in_split = true;
-
-var mcags;
-
-var cog_annotated = false, kegg_annotated = false;
+var thresh_count_gene_colors = 1;
+var order_gene_colors_by_count = true;
function loadAll() {
info("Initiated");
@@ -218,21 +219,6 @@ function loadAll() {
info("Checking for gene functional annotations");
geneParser = new GeneParser(genes);
- geneParser["data"].forEach(function(gene) {
- if(gene.functions != null) {
- if(gene.functions.hasOwnProperty("COG20_CATEGORY")) {
- gene.functions["COG_CATEGORY"] = gene.functions["COG20_CATEGORY"];
- gene.functions["COG_FUNCTION"] = gene.functions["COG20_FUNCTION"];
- } else if(gene.functions.hasOwnProperty("COG14_CATEGORY")) {
- gene.functions["COG_CATEGORY"] = gene.functions["COG14_CATEGORY"];
- gene.functions["COG_FUNCTION"] = gene.functions["COG14_FUNCTION"];
- }
-
- if(gene.functions.hasOwnProperty("COG_CATEGORY")) cog_annotated = true;
- if(gene.functions.hasOwnProperty("KEGG_Class")) kegg_annotated = true;
- if(cog_annotated && kegg_annotated) return;
- }
- });
if(!state['highlight-genes']) state['highlight-genes'] = {};
state['large-indel'] = 10;
@@ -279,25 +265,14 @@ function loadAll() {
state['source-colors'] = default_source_colors;
}
generateFunctionColorTable(state['source-colors'], "Source", highlight_genes=state['highlight-genes'], show_cags_in_split);
- mcags = Object.keys(default_source_colors);
- if(cog_annotated) {
- $('#gene_color_order').append($('', {
- value: 'COG',
- text: 'COG'
- }));
-
- if(!state.hasOwnProperty('cog-colors')) {
- state['cog-colors'] = default_COG_colors
- }
- }
- if(kegg_annotated) {
+ for(fn of getFunctionalAnnotations()) {
$('#gene_color_order').append($(' ', {
- value: 'KEGG',
- text: 'KEGG'
+ value: fn,
+ text: fn
}));
-
- if(!state.hasOwnProperty('kegg-colors')) {
- state['kegg-colors'] = default_KEGG_colors
+ let prop = fn.toLowerCase() + '-colors';
+ if(!state.hasOwnProperty(prop)) {
+ state[prop] = getCustomColorDict(fn);
}
}
@@ -457,21 +432,18 @@ function loadAll() {
}).change(function() {
state['gene-fn-db'] = $(this).val();
- switch($(this).val()) {
- case "COG":
- mcags = Object.keys(COG_categories);
- break;
- case "KEGG":
- mcags = Object.keys(KEGG_categories);
- break;
- case "Source":
- mcags = Object.keys(default_source_colors);
- }
resetFunctionColors(state[$(this).val().toLowerCase() + '-colors']);
redrawArrows();
$(this).blur();
});
+ $('#thresh_count').on('keydown', function(e) {
+ if (e.keyCode == 13) { // 13 = enter key
+ filterColorTable($(this).val());
+ $(this).blur();
+ }
+ });
+
$('#largeIndelInput').on('keydown', function(e) {
if(e.keyCode == 13) { // 13 = enter key
if($(this).val() < 1 || $(this).val() > 9999) {
@@ -611,61 +583,74 @@ function get_box_id_for_AA(aa, id_start) {
return id;
}
-/*
- * Generates KEGG color table html given a color palette.
- *
- * Params:
- * - fn_colors: a dictionary matching each category to a hex color code
- * - fn_type: a string indicating function category type: one of "COG", "KEGG", "Source"
- * - highlight_genes: a dictionary matching specific gene caller IDs to hex colors to override other coloring
- * - filter_to_split: if true, filters categories to only those shown in the split
- */
-function generateFunctionColorTable(fn_colors, fn_type, highlight_genes={}, filter_to_split) {
+
+ /*
+ * Generates functional annotation color table for a given color palette.
+ *
+ * @param fn_colors : dict matching each category to a hex color code to override defaults
+ * @param fn_type : string indicating function category type
+ * @param highlight_genes : a dictionary matching specific gene caller IDs to hex colors to override other coloring
+ * @param filter_to_split : if true, filters categories to only those shown in the split
+ * @param sort_by_count : if true, sort annotations by # occurrences, otherwise sort alphabetically
+ * @param thresh_count : int indicating min # occurences required for a given category to be included in the table
+ */
+function generateFunctionColorTable(fn_colors, fn_type, highlight_genes=null, filter_to_split=true, sort_by_count=order_gene_colors_by_count, thresh_count=thresh_count_gene_colors) {
info("Generating gene functional annotation color table");
- var db = (function(type){
- switch(type) {
- case "COG":
- return COG_categories;
- case "KEGG":
- return KEGG_categories;
- case "Source":
- return default_source_colors;
- default:
- console.log("Warning: Invalid type for function color table");
- return null;
- }
- })(fn_type);
- if(db == null) return;
- $('#tbody_function_colors').empty();
+ let db, counts;
+ if(fn_type == 'Source') {
+ db = default_source_colors;
+ } else {
+ counts = [];
- if(fn_type != "Source" && filter_to_split) {
- var new_colors = {};
+ // Traverse categories
for(gene of genes) {
- if(gene.functions && gene.functions[fn_type + "_CATEGORY"]) {
- new_colors[gene.functions[fn_type + "_CATEGORY"][0][0]] = fn_colors[gene.functions[fn_type + "_CATEGORY"][0][0]]
- }
+ let cag = getCagForType(gene.functions, fn_type);
+ counts.push(cag ? cag : "None");
}
- if(Object.keys(new_colors).length > 0) fn_colors = new_colors;
- }
- Object.keys(db).forEach(function(category){
- if(!(category in fn_colors)) {
- return;
+ // Get counts for each category
+ counts = counts.reduce((counts, val) => {
+ counts[val] = counts[val] ? counts[val]+1 : 1;
+ return counts;
+ }, {});
+
+ // Filter by count
+ let count_removed = 0;
+ counts = Object.fromEntries(
+ Object.entries(counts).filter(([cag,count]) => {
+ if(count < thresh_count) count_removed += count;
+ return count >= thresh_count || cag == "None";
+ })
+ );
+
+ // Save pre-sort order
+ let order = {};
+ for(let i = 0; i < Object.keys(counts).length; i++) {
+ order[Object.keys(counts)[i]] = i;
}
- var category_name = (fn_type == "Source" ? category : db[category]);
-
- var tbody_content =
- ' \
- \
- \
-
\
- \
- ' + category_name + ' \
- ';
-
- $('#tbody_function_colors').append(tbody_content);
- });
+
+ // Sort categories
+ counts = Object.fromEntries(
+ Object.entries(counts).sort(function(first, second) {
+ return sort_by_count ? second[1] - first[1] : first[0].localeCompare(second[0]);
+ })
+ );
+ if(count_removed > 0) counts["Other"] = count_removed;
+
+ // Create custom color dict from categories
+ db = getCustomColorDict(fn_type, cags=Object.keys(counts), order=order);
+ }
+
+ // Override default values with any values supplied to fn_colors
+ if(fn_colors) {
+ Object.keys(db).forEach(cag => { if(Object.keys(fn_colors).includes(cag)) db[cag] = fn_colors[cag] });
+ }
+
+ $('#tbody_function_colors').empty();
+ Object.keys(db).forEach(category =>
+ appendColorRow(fn_type == 'Source' ? category : category + " (" + counts[category] + ")", category, db[category])
+ );
$('.colorpicker').colpick({
layout: 'hex',
@@ -682,39 +667,25 @@ function generateFunctionColorTable(fn_colors, fn_type, highlight_genes={}, filt
$(this).colpickSetColor(this.value);
});
- if(!isEmpty(highlight_genes)) {
-
+ if(highlight_genes) {
for(gene of genes) {
- var gene_callers_id = "" + gene.gene_callers_id;
- if(Object.keys(highlight_genes).includes(gene_callers_id)) {
- var tbody_content =
- ' \
- \
- \
-
\
- \
- Gene ID: ' + gene_callers_id + ' \
- ';
-
- $('#tbody_function_colors').prepend(tbody_content);
-
- $('#picker_' + gene_callers_id).colpick({
- layout: 'hex',
- submit: 0,
- colorScheme: 'light',
- onChange: function(hsb, hex, rgb, el, bySetColor) {
- $(el).css('background-color', '#' + hex);
- $(el).attr('color', '#' + hex);
-
- state['highlight-genes'][el.id.substring(7)] = '#' + hex;
- if (!bySetColor) $(el).val(hex);
- }
- }).keyup(function() {
- $(this).colpickSetColor(this.value);
- });
- }
+ let gene_id = "" + gene.gene_callers_id;
+ if(Object.keys(highlight_genes).includes(gene_id)) appendColorRow("Gene ID: " + gene_id, gene_id, highlight_genes[gene_id], prepend=true);
}
+ $('.colorpicker').colpick({
+ layout: 'hex',
+ submit: 0,
+ colorScheme: 'light',
+ onChange: function(hsb, hex, rgb, el, bySetColor) {
+ $(el).css('background-color', '#' + hex);
+ $(el).attr('color', '#' + hex);
+ state[$('#gene_color_order').val().toLowerCase() + '-colors'][el.id.substring(7)] = '#' + hex;
+ if (!bySetColor) $(el).val(hex);
+ }
+ }).keyup(function() {
+ $(this).colpickSetColor(this.value);
+ });
}
}
@@ -769,18 +740,9 @@ function toggleGeneIDColor(gene_id, color="#FF0000") {
function addGeneIDColor(gene_id, color="#FF0000") {
state['highlight-genes'][gene_id] = color;
- var tbody_content =
- ' \
- \
- \
-
\
- \
- Gene ID: ' + gene_id + ' \
- ';
-
- $('#tbody_function_colors').prepend(tbody_content);
-
- $('#picker_' + gene_id).colpick({
+ appendColorRow("Gene ID: " + gene_id, gene_id, color, prepend=true);
+
+ $('.colorpicker').colpick({
layout: 'hex',
submit: 0,
colorScheme: 'light',
@@ -826,14 +788,17 @@ function redrawArrows() {
drawArrows(parseInt($('#brush_start').val()), parseInt($('#brush_end').val()), $('#gene_color_order').val(), gene_offset_y, Object.keys(state['highlight-genes']));
}
-function resetArrowMarkers() {
- info("Resetting arrow markers");
- $('#contextSvgDefs').empty();
-
- ["none"].concat(mcags).concat(Object.keys(state['highlight-genes'])).forEach(function(category){
- contextSvg.select('#contextSvgDefs')
- .append('svg:marker')
- .attr('id', 'arrow_' + category)
+function defineArrowMarkers(fn_type, cags=null, noneMarker=true) {
+ if(!cags) cags = Object.keys(getCustomColorDict(fn_type));
+ if(noneMarker) cags = ["None"].concat(cags);
+ cags.forEach(category => {
+ if(category.indexOf(',') != -1) category = category.substr(0,category.indexOf(','));
+ if(category.indexOf(';') != -1) category = category.substr(0,category.indexOf(';'));
+ if(category.indexOf('!!!') != -1) category = category.substr(0,category.indexOf('!!!'));
+ category = getCleanCagCode(category);
+ let color = $('#picker_' + category).length > 0 ? $('#picker_' + category).attr('color') : $('#picker_Other').attr('color');
+ contextSvg.select('#contextSvgDefs').append('svg:marker')
+ .attr('id', 'arrow_' + category )
.attr('markerHeight', 2)
.attr('markerWidth', 2)
.attr('orient', 'auto')
@@ -842,10 +807,18 @@ function resetArrowMarkers() {
.attr('viewBox', '-5 -5 10 10')
.append('svg:path')
.attr('d', 'M 0,0 m -5,-5 L 5,0 L -5,5 Z')
- .attr('fill', category != "none" ? $('#picker_' + category).attr('color') : "gray");
+ .attr('fill', color);
});
}
+function resetArrowMarkers() {
+ info("Resetting arrow markers");
+ $('#contextSvgDefs').empty();
+
+ defineArrowMarkers($('#gene_color_order').val());
+ defineArrowMarkers(null, cags=Object.keys(state['highlight-genes']), noneMarker=false);
+}
+
/*
* Sets gene colors for the selected category type to the default set
*
@@ -855,20 +828,10 @@ function resetArrowMarkers() {
function resetFunctionColors(fn_colors=null) {
info("Resetting functional annotation colors");
if($('#gene_color_order') == null) return;
+ let prop = $('#gene_color_order').val().toLowerCase() + '-colors';
+ Object.assign(state[prop], fn_colors ? fn_colors : getCustomColorDict($('#gene_color_order')));
- switch($('#gene_color_order').val()) {
- case 'Source':
- Object.assign(state['source-colors'], fn_colors ? fn_colors : default_source_colors);
- break;
- case 'COG':
- Object.assign(state['cog-colors'], fn_colors ? fn_colors : default_COG_colors);
- break;
- case 'KEGG':
- Object.assign(state['kegg-colors'], fn_colors ? fn_colors : default_KEGG_colors);
- break;
- }
-
- generateFunctionColorTable(state[$('#gene_color_order').val().toLowerCase() + '-colors'],
+ generateFunctionColorTable(state[prop],
$('#gene_color_order').val(),
state['highlight-genes'],
show_cags_in_split);
@@ -1499,8 +1462,10 @@ function saveState()
function processState(state_name, state) {
// set color defaults
if(!state['source-colors']) state['source-colors'] = default_source_colors;
- if(!state['cog-colors']) state['cog-colors'] = default_COG_colors;
- if(!state['kegg-colors']) state['kegg-colors'] = default_KEGG_colors;
+ for(fn of getFunctionalAnnotations()) {
+ let prop = fn.toLowerCase() + '-colors';
+ if(!state[prop]) state[prop] = getCustomColorDict(fn);
+ }
if(JSON.parse(localStorage.state) && JSON.parse(localStorage.state)['gene-fn-db']) {
state['gene-fn-db'] = JSON.parse(localStorage.state)['gene-fn-db'];
@@ -1510,7 +1475,6 @@ function processState(state_name, state) {
$('#gene_color_order').val(state['gene-fn-db']);
generateFunctionColorTable(state[state['gene-fn-db'].toLowerCase() + '-colors'], state['gene-fn-db'], highlight_genes=state['highlight-genes'], show_cags_in_split);
- mcags = Object.keys(state[state['gene-fn-db'].toLowerCase() + '-colors']);
this.state = state;
if(!state['highlight-genes']) {
@@ -1519,20 +1483,7 @@ function processState(state_name, state) {
if(!state['show_highlights']) state['show_highlights'] = true;
// define arrow markers for highlighted gene ids
- Object.keys(state['highlight-genes']).forEach(function(gene_id){
- contextSvg.select('#contextSvgDefs')
- .append('svg:marker')
- .attr('id', 'arrow_' + gene_id)
- .attr('markerHeight', 2)
- .attr('markerWidth', 2)
- .attr('orient', 'auto')
- .attr('refX', 0)
- .attr('refY', 0)
- .attr('viewBox', '-5 -5 10 10')
- .append('svg:path')
- .attr('d', 'M 0,0 m -5,-5 L 5,0 L -5,5 Z')
- .attr('fill', $('#picker_' + gene_id).attr('color'));
- });
+ defineArrowMarkers(null, cags=Object.keys(state['highlight-genes']), noneMarker=false);
redrawArrows();
if(state['show_indels']) {
@@ -1735,50 +1686,12 @@ function createCharts(state){
.attr("fill-opacity", "0.2")
.attr('transform', 'translate(50, 10)');
- // Define arrow markers
- if(cog_annotated) {
- ["none"].concat(Object.keys(COG_categories)).forEach(function(category){
- defs.append('svg:marker')
- .attr('id', 'arrow_' + category )
- .attr('markerHeight', 2)
- .attr('markerWidth', 2)
- .attr('orient', 'auto')
- .attr('refX', 0)
- .attr('refY', 0)
- .attr('viewBox', '-5 -5 10 10')
- .append('svg:path')
- .attr('d', 'M 0,0 m -5,-5 L 5,0 L -5,5 Z')
- .attr('fill', category != "none" ? $('#picker_' + category).attr('background-color') : "gray");
- });
- }
- if(kegg_annotated) {
- ["none"].concat(Object.keys(KEGG_categories)).forEach(function(category){
- defs.append('svg:marker')
- .attr('id', 'arrow_' + category )
- .attr('markerHeight', 2)
- .attr('markerWidth', 2)
- .attr('orient', 'auto')
- .attr('refX', 0)
- .attr('refY', 0)
- .attr('viewBox', '-5 -5 10 10')
- .append('svg:path')
- .attr('d', 'M 0,0 m -5,-5 L 5,0 L -5,5 Z')
- .attr('fill', category != "none" ? $('#picker_' + category).attr('background-color') : "gray");
- });
+
+ for(fn of getFunctionalAnnotations()) {
+ defineArrowMarkers(fn);
}
- ["none"].concat(Object.keys(default_source_colors)).concat(Object.keys(state['highlight-genes'])).forEach(function(category){
- defs.append('svg:marker')
- .attr('id', 'arrow_' + category )
- .attr('markerHeight', 2)
- .attr('markerWidth', 2)
- .attr('orient', 'auto')
- .attr('refX', 0)
- .attr('refY', 0)
- .attr('viewBox', '-5 -5 10 10')
- .append('svg:path')
- .attr('d', 'M 0,0 m -5,-5 L 5,0 L -5,5 Z')
- .attr('fill', category != "none" ? $('#picker_' + category).attr('color') : "gray");
- });
+ defineArrowMarkers("Source");
+ defineArrowMarkers(null, cags=Object.keys(state['highlight-genes']), noneMarker=false);
$('#context-container').css("width", (width + 150) + "px");
diff --git a/anvio/data/interactive/js/color-coding.js b/anvio/data/interactive/js/color-coding.js
index c6e830f205..2f589ed12d 100644
--- a/anvio/data/interactive/js/color-coding.js
+++ b/anvio/data/interactive/js/color-coding.js
@@ -1,8 +1,9 @@
/**
* Javascript library to visualize anvi'o gene clusterss
*
- * Author: Mahmoud Yousef
- * Copyright 2018, The anvio Project
+ * Authors: Mahmoud Yousef
+ *
+ * Copyright 2018-2021, The anvi'o project (http://anvio.org)
*
* This file is part of anvi'o ().
*
@@ -56,7 +57,7 @@ function colorAlgorithm(positions){
var _positions = []
for (aa in positions){
if (checked(positions[aa]) && aboveThreshold(positions, positions[aa])) {
- _positions[aa] = color(positions, positions[aa]);
+ _positions[aa] = color(positions, positions[aa]);
} else{
var dict = {}
dict[positions[aa]] = "BLACK";
@@ -80,7 +81,7 @@ function checked(letter){
//does the actual comparisons
//This checks for amino acid conservation by common characteristics
-function aboveThreshold(positions, aa) {
+function aboveThreshold(positions, aa) {
var number = 0;
for (acid in positions) {
if (acid === '') {
@@ -91,7 +92,7 @@ function aboveThreshold(positions, aa) {
var count = 0.0;
var count2 = 0.0;
var count3 = 0.0;
-
+
var letter = aa
switch (letter) {
case "A":
@@ -238,7 +239,7 @@ function aboveThreshold(positions, aa) {
break;
default: break;
}
- return false;
+ return false;
}
@@ -275,7 +276,7 @@ function color(positions, aa){
case "Y":
x = "DARKTURQUOISE";
break;
- case "P":
+ case "P":
x = "YELLOW";
break;
case "C":
@@ -315,15 +316,15 @@ function initializeCheckBoxes(){
box.onclick = ( function() {
return createDisplay();
} )
-
+
var label = document.createElement('label')
label.htmlFor = word;
label.style = "margin-left:1px;";
label.appendChild(document.createTextNode(word));
-
+
container.appendChild(box);
- container.appendChild(label);
- box.checked = true
+ container.appendChild(label);
+ box.checked = true
}
container.appendChild(document.createElement('br'));
diff --git a/anvio/data/interactive/js/constants.js b/anvio/data/interactive/js/constants.js
index 8e3161305f..3f966e95f3 100644
--- a/anvio/data/interactive/js/constants.js
+++ b/anvio/data/interactive/js/constants.js
@@ -1,29 +1,62 @@
-var COG_categories = {
- 'A': '[A] RNA processing and modification',
- 'B': '[B] Chromatin Structure and dynamics',
- 'C': '[C] Energy production and conversion',
- 'D': '[D] Cell cycle control and mitosis',
- 'E': '[E] Amino Acid metabolism and transport',
- 'F': '[F] Nucleotide metabolism and transport',
- 'G': '[G] Carbohydrate metabolism and transport',
- 'H': '[H] Coenzyme metabolis',
- 'I': '[I] Lipid metabolism',
- 'J': '[J] Translation',
- 'K': '[K] Transcription',
- 'L': '[L] Replication and repair',
- 'M': '[M] Cell wall/membrane/envelop biogenesis',
- 'N': '[N] Cell motility',
- 'O': '[O] Post-translational modification, protein turnover, chaperone functions',
- 'P': '[P] Inorganic ion transport and metabolism',
- 'Q': '[Q] Secondary Structure',
- 'T': '[T] Signal Transduction',
- 'U': '[U] Intracellular trafficing and secretion',
- 'V': '[V] Defense mechanisms',
- 'W': '[W] Extracellular structures',
- 'Y': '[Y] Nuclear structure',
- 'Z': '[Z] Cytoskeleton',
- 'R': '[R] General Functional Prediction only',
- 'S': '[S] Function Unknown'
+/**
+ * Constants used from various interface functions
+ *
+ * Authors: A. Murat Eren
+ * Isaac Fink
+ * Ozcan Esen
+ *
+ * Copyright 2015-2021, The anvi'o project (http://anvio.org)
+ *
+ * Anvi'o is a free software. You can redistribute this program
+ * and/or modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation, either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with anvi'o. If not, see .
+ *
+ * @license GPL-3.0+
+ */
+
+ var COG_categories = {
+ 'A': '[A] RNA processing and modification',
+ 'B': '[B] Chromatin Structure and dynamics',
+ 'C': '[C] Energy production and conversion',
+ 'D': '[D] Cell cycle control and mitosis',
+ 'E': '[E] Amino Acid metabolism and transport',
+ 'F': '[F] Nucleotide metabolism and transport',
+ 'G': '[G] Carbohydrate metabolism and transport',
+ 'H': '[H] Coenzyme metabolis',
+ 'I': '[I] Lipid metabolism',
+ 'J': '[J] Translation',
+ 'K': '[K] Transcription',
+ 'L': '[L] Replication and repair',
+ 'M': '[M] Cell wall/membrane/envelop biogenesis',
+ 'N': '[N] Cell motility',
+ 'O': '[O] Post-translational modification, protein turnover, chaperone functions',
+ 'P': '[P] Inorganic ion transport and metabolism',
+ 'Q': '[Q] Secondary Structure',
+ 'T': '[T] Signal Transduction',
+ 'U': '[U] Intracellular trafficing and secretion',
+ 'V': '[V] Defense mechanisms',
+ 'W': '[W] Extracellular structures',
+ 'Y': '[Y] Nuclear structure',
+ 'Z': '[Z] Cytoskeleton',
+ 'R': '[R] General Functional Prediction only',
+ 'S': '[S] Function Unknown'
+ }
+
+ var KEGG_categories = {
+ 'C': 'Carbohydrate metabolism',
+ 'E': 'Energy metabolism',
+ 'L': 'Lipid metabolism',
+ 'N': 'Nucleotide metabolism',
+ 'A': 'Amino acid metabolism',
+ 'G': 'Glycan biosynthesis and metabolism',
+ 'V': 'Metabolism of cofactors and vitamins',
+ 'T': 'Metabolism of terpenoids and polyketides',
+ 'S': 'Biosynthesis of other secondary metabolites',
+ 'X': 'Xenobiotics biodegradation and metabolism'
}
var default_COG_colors = {
@@ -54,19 +87,6 @@ var default_COG_colors = {
'S': '#81402e'
}
-var KEGG_categories = {
- 'C': 'Carbohydrate metabolism',
- 'E': 'Energy metabolism',
- 'L': 'Lipid metabolism',
- 'N': 'Nucleotide metabolism',
- 'A': 'Amino acid metabolism',
- 'G': 'Glycan biosynthesis and metabolism',
- 'V': 'Metabolism of cofactors and vitamins',
- 'T': 'Metabolism of terpenoids and polyketides',
- 'S': 'Biosynthesis of other secondary metabolites',
- 'X': 'Xenobiotics biodegradation and metabolism'
-}
-
var default_KEGG_colors = {
'C': '#0000ee',
'E': '#9933cc',
@@ -87,6 +107,9 @@ var default_source_colors = {
'rRNA': '#b22222'
}
+// default colors for user-supplied functional annotations
+var custom_cag_colors = Object.values(default_COG_colors).concat(Object.values(default_KEGG_colors));
+
var named_functional_sources = {
'EGGNOG_BACT': {
'accession_decorator': (function (d) {
diff --git a/anvio/data/interactive/js/context-menu.js b/anvio/data/interactive/js/context-menu.js
index 8b5fd5b41f..d78a28419f 100644
--- a/anvio/data/interactive/js/context-menu.js
+++ b/anvio/data/interactive/js/context-menu.js
@@ -1,9 +1,9 @@
/**
- * Handles right click menu
+ * Handles right click menu functions
*
- * Author: Özcan Esen
- * Credits: A. Murat Eren
- * Copyright 2018, The anvio Project
+ * Authors: Özcan Esen
+ * A. Murat Eren
+ * Matthew Klein
*
* This file is part of anvi'o ().
*
@@ -17,6 +17,7 @@
*
* @license GPL-3.0+
*/
+
let outerLimit1;
let outerLimit2;
diff --git a/anvio/data/interactive/js/contigs-plot.js b/anvio/data/interactive/js/contigs-plot.js
index e6f4f8f8e9..a616d73031 100644
--- a/anvio/data/interactive/js/contigs-plot.js
+++ b/anvio/data/interactive/js/contigs-plot.js
@@ -1,3 +1,21 @@
+/**
+ * Contigs db stats visualization
+ *
+ * Authors: Ozcan Esen
+ *
+ * Copyright 2015-2021, The anvi'o project (http://anvio.org)
+ *
+ * Anvi'o is a free software. You can redistribute this program
+ * and/or modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation, either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with anvi'o. If not, see .
+ *
+ * @license GPL-3.0+
+ */
+
function draw_n_values_plot(container, stats) {
var svg = d3.select(container)
.append('svg')
@@ -45,7 +63,7 @@ function draw_n_values_plot(container, stats) {
.attr('height', tooltip_pos.height)
.attr('fill-opacity', '0.7')
.attr('width', tooltip_pos.width);
-
+
var tooltip_text = tooltip.append('text')
.attr('x', '10')
.attr('y', '15')
@@ -162,7 +180,7 @@ function draw_gene_counts_chart(container, gene_counts) {
.attr('height', tooltip_pos.height)
.attr('fill-opacity', '0.7')
.attr('width', tooltip_pos.width);
-
+
var tooltip_text = tooltip.append('text')
.attr('x', '10')
.attr('y', '15')
diff --git a/anvio/data/interactive/js/drawer.js b/anvio/data/interactive/js/drawer.js
index efbf711e34..9f55d9ff8e 100644
--- a/anvio/data/interactive/js/drawer.js
+++ b/anvio/data/interactive/js/drawer.js
@@ -1,11 +1,10 @@
/**
- * Javascript library to display phylogenetic trees
+ * Javascript library to display phylogenetic trees and more
*
- * Author: Özcan Esen
- * Credits: A. Murat Eren
- * Copyright 2015, The anvio Project
+ * Authors: Özcan Esen
+ * A. Murat Eren
*
- * This file is part of anvi'o ().
+ * Copyright 2015-2021, The anvi'o project (http://anvio.org)
*
* Anvi'o is a free software. You can redistribute this program
* and/or modify it under the terms of the GNU General Public
diff --git a/anvio/data/interactive/js/error.js b/anvio/data/interactive/js/error.js
index daf16aab36..4873d0a14d 100644
--- a/anvio/data/interactive/js/error.js
+++ b/anvio/data/interactive/js/error.js
@@ -15,13 +15,13 @@ let ERROR_COUNT = 0
const issueCategories = [
{
- 'category' : 'Dependencies failed to load',
+ 'category' : 'Dependencies failed to load',
'content' : `This can occur when git submodules that Anvi'o relies on fail to load. If you are tracking the main development branch of Anvi'o,
try running git submodule update --init `
},
{
- 'category' : 'ReferenceError - ___ is not defined',
- 'content' : `This can occur when Anvi'o wants to utilize some variable which it cannot resolve. Until we get some better troubleshooting advice here,
+ 'category' : 'ReferenceError - ___ is not defined',
+ 'content' : `This can occur when Anvi'o wants to utilize some variable which it cannot resolve. Until we get some better troubleshooting advice here,
try running your interactive session with the --debug flag`
},
]
@@ -30,13 +30,13 @@ function alertDependencyError(dependencyError, isFinalDependency){
if(dependencyError){
ERROR_COUNT += 1
}
- if(isFinalDependency && ERROR_COUNT){ // hacky way of 'iterating' all dependency calls before error messaging
+ if(isFinalDependency && ERROR_COUNT){ // hacky way of 'iterating' all dependency calls before error messaging
displayAlert('dependencies')
}
}
function displayAlert(error){
- let reason;
+ let reason;
if(error == 'dependencies'){
reason = 'loading dependencies'
@@ -50,7 +50,7 @@ function displayAlert(error){
function errorLandingContext(){ // onload function called by error-landing.html, generate help 'docs' from object above
issueCategories.map((issue, idx) => {
- document.querySelector('#content-div').innerHTML +=
+ document.querySelector('#content-div').innerHTML +=
`
${issue.category} ∆
diff --git a/anvio/data/interactive/js/geneclusters.js b/anvio/data/interactive/js/geneclusters.js
index 30583cef8e..c4ef5ed483 100644
--- a/anvio/data/interactive/js/geneclusters.js
+++ b/anvio/data/interactive/js/geneclusters.js
@@ -1,11 +1,11 @@
/**
* Javascript library to visualize anvi'o gene clusterss
*
- * Author: A. Murat Eren
- * Credits: Özcan Esen
- * Copyright 2016, The anvio Project
+ * Authors: A. Murat Eren
+ * Özcan Esen
+ * Mahmoud Yousef
*
- * This file is part of anvi'o ().
+ * Copyright 2016-2021, The anvi'o project (http://anvio.org)
*
* Anvi'o is a free software. You can redistribute this program
* and/or modify it under the terms of the GNU General Public
@@ -162,7 +162,7 @@ function createDisplay(){
if (gene_cluster_data.genomes.indexOf(layer) === -1)
continue;
-
+
var rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
rect.setAttribute('x', 0);
rect.setAttribute('y', y_cord);
@@ -187,7 +187,7 @@ function createDisplay(){
sub_y_cord = y_cord + 5;
gene_cluster_data.gene_caller_ids_in_genomes[layer].forEach(function (caller_id) {
- sequence = gene_cluster_data.aa_sequences_in_gene_cluster[layer][caller_id];
+ sequence = gene_cluster_data.aa_sequences_in_gene_cluster[layer][caller_id];
var text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
text.setAttribute('x', 0);
text.setAttribute('y', sub_y_cord);
@@ -232,7 +232,7 @@ function createDisplay(){
tspan.appendChild(document.createTextNode(acid));
tspan.setAttribute('style', 'alignment-baseline:text-before-edge');
text.appendChild(tspan);
- }
+ }
fragment.appendChild(text);
@@ -267,7 +267,7 @@ function createDisplay(){
$('[data-toggle="popover"]').on('shown.bs.popover', function (e) {
var popover = $(e.target).data("bs.popover").$tip;
-
+
if ($(popover).css('top').charAt(0) === '-') {
$(popover).css('top', '0px');
}
diff --git a/anvio/data/interactive/js/genomeview/drawer.js b/anvio/data/interactive/js/genomeview/drawer.js
new file mode 100644
index 0000000000..dd6af73019
--- /dev/null
+++ b/anvio/data/interactive/js/genomeview/drawer.js
@@ -0,0 +1,930 @@
+/**
+ * Javascript library for anvi'o genome view
+ *
+ * Authors: Isaac Fink
+ * Matthew Klein
+ * A. Murat Eren
+ *
+ * Copyright 2021, The anvi'o project (http://anvio.org)
+ *
+ * Anvi'o is a free software. You can redistribute this program
+ * and/or modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation, either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with anvi'o. If not, see .
+ *
+ * @license GPL-3.0+
+ */
+
+/**
+ * File Overview : The Drawer class defined here is responsible for rendering genomic + associated data passed from main.js to an interactive
+ * browser canvas. This is where most of the heavy lifting should happen, and where most of our Fabric.js library interactions should occur.
+ */
+var GenomeDrawer = function (settings) {
+ this.settings = settings;
+};
+
+GenomeDrawer.prototype.draw = function () {
+ canvas.clear()
+ labelSpacing = 30 // reset to default value upon each draw() call
+ canvas.setHeight(calculateMainCanvasHeight()) // set canvas height dynamically
+
+ this.settings['genomeData']['genomes'].map((genome, idx) => {
+ this.addLayers(idx)
+ labelSpacing += 30
+ })
+
+ checkGeneLabels();
+ drawTestShades();
+ $('.loading-screen').hide();
+
+ if(firstDraw){
+ this.setInitialZoom()
+ firstDraw = false
+ }
+}
+
+/*
+ * For each genome group, iterate additional all layers and render where appropriate
+ */
+GenomeDrawer.prototype.addLayers = function (orderIndex) {
+ let [dataLayerHeight, rulerHeight] = [this.calculateLayerSizes()[0], this.calculateLayerSizes()[1]]
+
+ yOffset = orderIndex * spacing + (orderIndex * maxGroupSize * groupLayerPadding);
+ let layerPos = 0
+ let genomeID = this.settings['genomeData']['genomes'][orderIndex][0];
+ // let genome = this.settings['genomeData']['genomes'][orderIndex][1];
+ // let label = genome.genes.gene_calls[0].contig;
+
+ let additionalDataLayers = this.settings['additional-data-layers']['data'][genomeID]
+
+ let ptInterval = Math.floor(genomeMax / adlPtsPerLayer);
+
+ this.settings['group-layer-order'].map((layer, idx) => { // render out layers, ordered via group-layer-order array
+ if (layer == 'Genome' && $('#Genome-show').is(':checked')) {
+ this.addGenome(orderIndex, dataLayerHeight, layerPos)
+ layerPos += dataLayerHeight + groupLayerPadding
+ }
+ if (layer == 'Coverage' && this.settings['additional-data-layers']['layers'].includes('Coverage') && $('#Coverage-show').is(':checked')) {
+ this.buildNumericalDataLayer('Coverage', layerPos, genomeID, additionalDataLayers, ptInterval, 'blue', dataLayerHeight, orderIndex)
+ layerPos += dataLayerHeight + groupLayerPadding
+ }
+ if (layer == 'GC_Content' && this.settings['additional-data-layers']['layers'].includes('GC_content') && $('#GC_Content-show').is(':checked')) {
+ this.buildNumericalDataLayer('GC_content', layerPos, genomeID, additionalDataLayers, ptInterval, 'purple', dataLayerHeight, orderIndex)
+ layerPos += dataLayerHeight + groupLayerPadding
+ }
+ if (layer == 'Ruler' && this.settings['additional-data-layers']['layers'].includes('ruler') && $('#Ruler-show').is(':checked')) {
+ this.buildGroupRulerLayer(genomeID, layerPos, rulerHeight, orderIndex)
+ layerPos += rulerHeight + groupLayerPadding
+ }
+ })
+
+ canvas.remove(canvas.getObjects().find(obj => obj.id == 'groupBorder' + orderIndex));
+ this.addGroupBorder(yOffset, orderIndex)
+}
+
+/*
+ * add a stylish and visually significant border around each group
+ */
+GenomeDrawer.prototype.addGroupBorder = function (yOffset, orderIndex) {
+
+ let top = yOffset + marginTop - 20 + (orderIndex * groupMargin)
+ let left = 0
+ let width = genomeMax
+ let height = spacing + 60
+
+ let rect = new fabric.Rect({
+ id: 'groupBorder' + orderIndex,
+ top: top,
+ left: left,
+ width: width,
+ height: height,
+ stroke: 'pink',
+ strokeWidth: 2,
+ fill: "pink",
+ opacity: .2,
+ selectable: false,
+ })
+
+ canvas.sendToBack(rect)
+}
+
+/*
+ * programmatically calculate layer height values, given that the ruler layer should be allocated comparatively less space
+ */
+GenomeDrawer.prototype.calculateLayerSizes = function () {
+ let parityHeight = spacing / maxGroupSize
+ let rulerHeight = Math.floor(parityHeight * .5) // some arbitrary percentage of parity since ruler should get less y-axis space
+
+ // with the extra space carved out by a smaller rulerHeight, distribute the excess evenly amongst all layers that are NOT rulers
+ let dataLayerHeight = Math.floor(parityHeight * (1 + (.5 / (maxGroupSize - 1))))
+ return [dataLayerHeight, rulerHeight]
+}
+
+GenomeDrawer.prototype.addGenome = function (orderIndex, layerHeight, layerPos) {
+ let genome = this.settings['genomeData']['genomes'][orderIndex];
+ let gene_list = genome[1].genes.gene_calls;
+ let genomeID = genome[0];
+ let y = marginTop + yOffset + layerPos + (layerHeight / 2) + (orderIndex * groupMargin) // render arrows in the center of genome layer's allotted vertical space
+
+ if (showLabels) {
+ canvas.add(new fabric.Text(genomeID, { top: y - 5, selectable: false, fontSize: genomeLabelSize, fontFamily: 'sans-serif', fontWeight: 'bold' }));
+ }
+
+ let [start, stop] = percentScale ? getRenderXRangeForFrac() : renderWindow.map(x => x * scaleFactor + xDisps[genomeID]);
+ start = clamp(start > xDisps[genomeID] ? start : xDisps[genomeID], calcXBounds()[0], calcXBounds()[1]);
+ stop = clamp(stop, calcXBounds()[0], calcXBounds()[1]);
+
+ // line
+ let lineObj = new fabric.Line([start, 0, stop, 0], {
+ id: 'genomeLine',
+ groupID: genomeID,
+ top: y + 4,
+ stroke: 'black',
+ strokeWidth: 2,
+ selectable: false,
+ lockMovementY: true,
+ hasControls: false,
+ hasBorders: false,
+ lockScaling: true
+ });
+ canvas.add(lineObj);
+ this.addBackgroundShade((marginTop + yOffset + layerPos + (orderIndex * groupMargin)), start, genomeMax, layerHeight, orderIndex)
+
+ // draw set labels
+ if (showGeneLabels && settings['display']['labels']['gene-sets'][genomeID]) {
+ settings['display']['labels']['gene-sets'][genomeID].forEach(obj => {
+ drawSetLabel(obj[0], genomeID, obj[1]);
+ });
+ }
+
+ for (let geneID in gene_list) {
+ let gene = gene_list[geneID];
+ let [ntStart, ntStop] = getRenderNTRange(genomeID);
+ if (gene.start < ntStart) continue;
+ if (gene.stop > ntStop) return;
+ var geneObj = this.geneArrow(gene, geneID, y, genomeID, this.settings['display']['arrow-style']);
+ canvas.add(geneObj)
+ canvas.bringToFront(geneObj);
+
+ if (showGeneLabels) {
+ var label = new fabric.IText(setGeneLabelFromSource(geneID, genomeID), {
+ id: 'geneLabel',
+ groupID: genomeID,
+ fontSize: geneLabelSize,
+ angle: geneLabelPos == "above" ? -1 * geneLabelAngle : 0,
+ left: xDisps[genomeID] + (gene.start + 50) * scaleFactor,
+ scaleX: 0.5,
+ scaleY: 0.5,
+ editable: true,
+ hasControls: false,
+ opacity: settings['display']['hidden']?.[genomeID]?.[geneID] ? .2 : 1.0,
+ lockMovementX: true,
+ lockMovementY: true,
+ lockScaling: true,
+ hoverCursor: 'text'
+ });
+ if (this.settings['display']['arrow-style'] == 3) {
+ label.set({
+ top: geneLabelPos == "inside" ? y + 15 - geneLabelSize / 2 : y - 10 - geneLabelSize / 2,
+ selectionColor: 'rgba(128,128,128,.5)'
+ });
+ } else {
+ label.set({
+ top: y - 10 - geneLabelSize / 2,
+ selectionColor: 'rgba(128,128,128,.2)'
+ });
+ }
+ label.on("editing:exited", function (e) {
+ console.log(label.text)
+ });
+ canvas.add(label);
+ }
+
+ function setGeneLabelFromSource(geneID, genomeID) {
+ let genomeOfInterest = this.settings['genomeData']['genomes'].filter(genome => genome[0] == genomeID)
+ let source = $('#gene_label_source').val()
+
+ if (source == 'default') {
+ return `${geneID}`
+ }
+ if (source == 'user') {
+ if (this.settings['display']?.hasOwnProperty('gene-labels')) {
+ return this.settings['display']['gene-labels'][genomeID][geneID]
+ } else {
+ return 'None'
+ }
+ } else {
+ // operating under the assumption that
+ // A) the relevant source value lives at index 1 of the source array
+ // B) the selected value from the #gene_label_source dropdown === the source object itself (and it should, since we build the dropdown programmatically!)
+ // this below logic should retrieve the correct annotation value, regardless of source
+ if (genomeOfInterest[0][1]['genes']['functions'][geneID]?.hasOwnProperty(source) && genomeOfInterest[0][1]['genes']['functions'][geneID][source]) {
+ return ellipsisMachine(genomeOfInterest[0][1]['genes']['functions'][geneID][source][1])
+ } else {
+ return 'None'
+ }
+ }
+ }
+
+ function ellipsisMachine(string) { // add ellipsis only to truncated gene label values
+ if (string.substring(0, 20).length == 20) {
+ return `${string.substring(0, 20)}...`
+ } else {
+ return string
+ }
+ }
+ }
+
+ function drawSetLabel(title, genomeID, geneIDs) {
+ // assume gene IDs form contiguous list
+ let geneObjs = geneIDs.map(geneID => settings['genomeData']['genomes'].find(obj => obj[0] == genomeID)[1].genes.gene_calls[geneID]);
+ let x_set_label = geneObjs[0].start + (geneObjs[geneObjs.length - 1].stop - geneObjs[0].start) / 2;
+ let y_set_label = y - 10 - geneLabelSize;
+ var set_label = new fabric.IText(title, {
+ id: 'setLabel',
+ groupID: genomeID,
+ fontSize: geneLabelSize,
+ left: xDisps[genomeID] + x_set_label * scaleFactor,
+ top: y_set_label,
+ scaleX: 0.5,
+ scaleY: 0.5,
+ editable: true,
+ hasControls: false,
+ lockMovementX: true,
+ lockMovementY: true,
+ lockScaling: true,
+ hoverCursor: 'text',
+ });
+ canvas.add(set_label);
+ }
+}
+
+/*
+ * Process to generate numerical ADL for genome groups (ie Coverage, GC Content )
+ */
+GenomeDrawer.prototype.buildNumericalDataLayer = function (layer, layerPos, genomeID, additionalDataLayers, ptInterval, defaultColor, layerHeight, orderIndex) {
+ // TODO this will need to be refactored once we begin testing genomes comprised of multiple contigs
+ let contigObj = Object.values(additionalDataLayers)[0]
+ let contigArr = Object.values(contigObj)[0]
+ let stroke = 'black'
+
+ // if(layer == 'Coverage'){
+ // this.settings['display']['additional-data-layers']['coverage'] ? stroke = this.settings['display']['additional-data-layers']['coverage'] : stroke = 'black'
+ // }
+ if (layer == 'GC_content') { // we will need to refactor and get our variable case/formatting nonsense sorted.
+ this.settings['display']['colors']['GC_Content'] ? stroke = this.settings['display']['colors']['GC_Content'] : stroke = 'red'
+ }
+
+ let maxDataLayerValue = 0
+ let startingTop = marginTop + yOffset + layerPos + (orderIndex * groupMargin)
+ let startingLeft = xDisps[genomeID]
+
+ let globalPathDirective = [`L ${startingLeft} ${layerHeight}`]
+ let layer_end_final_coordinates
+
+ let pathDirective = [`M ${startingLeft} 0 L ${startingLeft} ${layerHeight}`]
+
+ for (let i = 0; i < contigArr.length; i++) {
+ contigArr[i] > maxDataLayerValue ? maxDataLayerValue = contigArr[i] : null
+ }
+
+ let nGroups = 20
+ let j = 0
+ let final_l = 0 //used to create final line segments to 'close out' path obj for shading purposes.
+ let [l, r] = getRenderNTRange(genomeID);
+ for (let i = 0; i < nGroups; i++) {
+ for (; j < i * genomeMax / nGroups; j += ptInterval) {
+ if (j < l) continue;
+ if (j > r) break;
+
+ let left = j * scaleFactor + startingLeft
+ let top = [contigArr[j] / maxDataLayerValue] * layerHeight
+ let segment = `L ${left} ${top}`
+ final_l = left // final_l is always last-seen x coordinate
+ pathDirective.push(segment)
+ globalPathDirective.push(segment)
+ }
+ // TODO resolve performance-related aspects of the chunking done below
+
+ // let graphObj = new fabric.Path(pathDirective.join(' '))
+ // graphObj.set({
+ // top : startingTop,
+ // stroke : stroke,
+ // fill : '',
+ // selectable: false,
+ // objectCaching: false,
+ // id : `${layer} graph`,
+ // groupID : genomeID,
+ // genome : genomeID
+ // })
+ // canvas.bringToFront(graphObj)
+ pathDirective = []
+ }
+ layer_end_final_coordinates = `L ${final_l} ${layerHeight} L ${startingLeft} ${layerHeight}`
+ globalPathDirective.push(layer_end_final_coordinates)
+
+ let shadedObj = new fabric.Path(globalPathDirective.join(' '))
+ shadedObj.set({
+ top: startingTop,
+ stroke: stroke,
+ fill: stroke,
+ selectable: false,
+ objectCaching: false,
+ id: `${layer}-graph-shaded`,
+ groupID: genomeID,
+ genome: genomeID
+ })
+ canvas.bringToFront(shadedObj)
+ this.addBackgroundShade(startingTop, startingLeft, genomeMax, layerHeight, orderIndex)
+}
+
+/*
+ * Generate individual genome group rulers
+ */
+GenomeDrawer.prototype.buildGroupRulerLayer = function (genomeID, layerPos, layerHeight, orderIndex) {
+ let startingTop = marginTop + yOffset + layerPos + (orderIndex * groupMargin)
+ let startingLeft = xDisps[genomeID]
+ // let layerHeight = (spacing / maxGroupSize)
+
+ // split ruler into several objects to avoid performance cost of large object pixel size
+ let nRulers = 20;
+ let w = 0;
+ let [l, r] = getRenderNTRange(genomeID);
+ for (let i = 0; i < nRulers; i++) {
+ let ruler = new fabric.Group();
+ for (; w < (i + 1) * genomeMax / nRulers; w += scaleInterval) {
+ if (w < l) continue;
+ if (w > r) break;
+ let tick = new fabric.Line([0, 0, 0, 20], {
+ left: (w * scaleFactor),
+ stroke: 'black',
+ strokeWidth: 1,
+ fontSize: 10,
+ fontFamily: 'sans-serif'
+ });
+ let lbl = new fabric.Text(w / 1000 + " kB", {
+ left: (w * scaleFactor + 5),
+ stroke: 'black',
+ strokeWidth: .25,
+ fontSize: 15,
+ fontFamily: 'sans-serif'
+ });
+ ruler.add(tick);
+ ruler.add(lbl);
+ }
+ ruler.set({
+ left: startingLeft,
+ top: startingTop + (layerHeight / 2),
+ lockMovementY: true,
+ hasControls: false,
+ hasBorders: false,
+ lockScaling: true,
+ objectCaching: false,
+ groupID: genomeID,
+ class: 'ruler'
+ });
+ ruler.addWithUpdate();
+ canvas.add(ruler);
+ }
+ this.addBackgroundShade(startingTop, startingLeft, genomeMax, layerHeight, orderIndex)
+}
+
+/*
+ * adds an alternating shade to each genome group for easier visual distinction amongst adjacent groups
+ */
+GenomeDrawer.prototype.addBackgroundShade = function (top, left, width, height, orderIndex) {
+ let backgroundShade;
+ orderIndex % 2 == 0 ? backgroundShade = '#b8b8b8' : backgroundShade = '#f5f5f5'
+
+ let border = new fabric.Rect({
+ groupID: this.settings['genomeData']['genomes'][orderIndex][0],
+ top: top,
+ left: left,
+ width: width,
+ height: height,
+ stroke: 'black',
+ strokeWidth: 2,
+ fill: "rgba(0,0,0,0.0)",
+ selectable: false,
+ // opacity : .5
+ });
+ let background = new fabric.Rect({
+ groupID: this.settings['genomeData']['genomes'][orderIndex][0],
+ top: top,
+ left: left,
+ width: width,
+ height: height,
+ fill: "#b0b0b0",
+ selectable: false,
+ opacity: .5
+ });
+ // canvas.sendToBack(border)
+ canvas.sendToBack(background)
+}
+
+GenomeDrawer.prototype.geneArrow = function (gene, geneID, y, genomeID, style) {
+ let ind = this.settings['genomeData']['genomes'].findIndex(g => g[0] == genomeID);
+ let functions = this.settings['genomeData']['genomes'][ind][1].genes.functions[geneID];
+
+ let color = $('#picker_None').length > 0 ? $('#picker_None').attr('color') : 'gray';
+ let cag = getCagForType(functions, color_db);
+
+ // TODO: use state instead of hardcoded color pickers
+
+ // check if gene is highlighted
+ // possibly deprecated - requires new table for user-defined colors
+ let pickerCode = genomeID + '-' + geneID;
+ if ($('#picker_' + pickerCode).length > 0) {
+ color = $('#picker_' + pickerCode).attr('color');
+ } else {
+ if (cag) {
+ cag = getCleanCagCode(cag);
+ let color_other = $('#picker_Other').length > 0 ? $('#picker_Other').attr('color') : 'white';
+ color = $('#picker_' + cag).length > 0 ? $('#picker_' + cag).attr('color') : color_other;
+ } else {
+ if (gene.source.startsWith('Ribosomal_RNA')) {
+ cag = 'rRNA';
+ } else if (gene.source == 'Transfer_RNAs') {
+ cag = 'tRNA';
+ } else if (gene.functions !== null) {
+ cag = 'Function';
+ }
+ if ($('#picker_' + cag).length > 0) color = $('#picker_' + cag).attr('color');
+ }
+ }
+
+ // check for set colors
+ if (settings['display']['colors']['genes'][genomeID] && settings['display']['colors']['genes'][genomeID][geneID]) {
+ color = settings['display']['colors']['genes'][genomeID][geneID];
+ }
+
+ let length = (gene.stop - gene.start) * scaleFactor;
+ let stemLength = length - 25 > 0 ? length - 25 : 0;
+
+ var arrowPathStr;
+ switch (parseInt(style)) {
+ case 2: // thicker arrows
+ arrowPathStr = `M ${stemLength} -10
+ L 0 -10
+ L 0 20
+ L ${stemLength} 20
+ L ${stemLength} 20
+ L ${stemLength} 20
+ L ${length} 5
+ L ${stemLength} -10 z`;
+ break;
+ case 3: // pentagon arrows
+ arrowPathStr = `M 0 0
+ L ${stemLength} 0
+ L ${length} 20
+ L ${stemLength} 40
+ L 0 40
+ L 0 0 z`;
+ break;
+ case 4: // rect arrows
+ arrowPathStr = `M ${length} -5
+ L 0 -5
+ L 0 15
+ L ${length} 15 z`;
+ break;
+ default: // 'inspect page' arrows
+ arrowPathStr = `M ${stemLength} -2.5
+ L 0 -2.5
+ L 0 12.5
+ L ${stemLength} 12.5
+ L ${stemLength} 20
+ L ${length} 5
+ L ${stemLength} -10 z`;
+ break;
+ }
+
+ var arrow = new fabric.Path(arrowPathStr);
+ let genomeOfInterest = settings['genomeData']['genomes'].filter(genome => genome[0] == genomeID)
+ arrow.set({
+ id: 'arrow',
+ groupID: genomeID,
+ lockMovementY: true,
+ selectable: true,
+ hasControls: false,
+ hasBorders: false,
+ lockScaling: true,
+ opacity: settings['display']['hidden']?.[genomeID]?.[geneID] ? .2 : 1.0,
+ aaSequence: genomeOfInterest[0][1]['genes']['aa'][geneID]['sequence'],
+ dnaSequence: genomeOfInterest[0][1]['genes']['dna'][geneID]['sequence'],
+ gene: gene,
+ functions: functions,
+ geneID: geneID,
+ genomeID: genomeID,
+ top: style == 3 ? y - 17 : y - 11, // TODO update this offset to reflect genome layer height (we want to render this arrow in the middle of its allocated height)
+ left: xDisps[genomeID] + (1.5 + gene.start) * scaleFactor,
+ fill: color,
+ stroke: 'gray',
+ strokeWidth: style == 3 ? 3 : 1.5
+ });
+ if (gene.direction == 'r') arrow.rotate(180);
+
+ return arrow;
+}
+
+/*
+ * Draw background shades between genes of the same cluster.
+ * TODO: generalize this function to take in [start,stop,val] NT sequence ranges to shade any arbitrary metric
+ * - add a separate function for retrieving [start,stop,val] given gene cluster IDs
+ *
+ * @param geneClusters : array of GC IDs to be shaded
+ * @param colors : dict defining color of each shade, in the form {geneClusterID : hexColor}
+ */
+GenomeDrawer.prototype.shadeGeneClusters = function (geneClusters, colors) {
+ if (!genomeData.gene_associations["anvio-pangenome"]) return;
+
+ let y = marginTop;
+ for (var i = 0; i < genomeData.genomes.length - 1; i++) {
+ let genomeA = this.settings['genomeData']['genomes'][i][1].genes.gene_calls;
+ let genomeB = this.settings['genomeData']['genomes'][i + 1][1].genes.gene_calls;
+ let genomeID_A = this.settings['genomeData']['genomes'][i][0];
+ let genomeID_B = this.settings['genomeData']['genomes'][i + 1][0];
+ let [l1, r1] = getRenderNTRange(genomeID_A);
+ let [l2, r2] = getRenderNTRange(genomeID_B);
+
+ for (gc of geneClusters) {
+ let g1 = [], g2 = [];
+ for (geneID of genomeData.gene_associations["anvio-pangenome"]["gene-cluster-name-to-genomes-and-genes"][gc][genomeID_A]) {
+ g1.push(genomeA[geneID].start, genomeA[geneID].stop);
+ }
+ for (geneID of genomeData.gene_associations["anvio-pangenome"]["gene-cluster-name-to-genomes-and-genes"][gc][genomeID_B]) {
+ g2.push(genomeB[geneID].start, genomeB[geneID].stop);
+ }
+
+ // if shades outside render bounds, don't draw them
+ if (g1[1] < l1 && g2[1] < l2) continue;
+ if (g1[0] > r1 && g2[0] > r2) break;
+
+ g1 = g1.map(val => val * scaleFactor + xDisps[genomeID_A]);
+ g2 = g2.map(val => val * scaleFactor + xDisps[genomeID_B]);
+
+ /* TODO: implementation for multiple genes of the same genome in the same gene cluster */
+ var path = new fabric.Path("M " + g1[0] + " " + y + " L " + g1[1] + " " + y + " L " + g2[1] + " " + (y + spacing) + " L " + g2[0] + " " + (y + spacing) + " z", {
+ id: 'link',
+ fill: colors[gc],
+ opacity: 0.25,
+ selectable: false
+ });
+ path.sendBackwards();
+ canvas.sendBackwards(path);
+ }
+ y += spacing
+ }
+}
+
+/*
+ * Add a temporary glow effect around given gene(s).
+ *
+ * @param geneParams : array of dicts, in one of two formats:
+ * (1) [{genomeID: gid_1, geneID: [id_1, id_2, ...]}, ...]
+ * (2) [{genomeID: gid_1, geneID: id_1}, ...]
+ */
+GenomeDrawer.prototype.glowGenes = function (geneParams, indefinite=false, timeInterval=5000) {
+ // convert geneParams format (1) to format (2)
+ if (Array.isArray(geneParams[0].geneID)) {
+ let newParams = [];
+ for (genome of geneParams) {
+ for (gene of genome.geneID) newParams.push({ genomeID: genome.genomeID, geneID: gene });
+ }
+ geneParams = newParams;
+ }
+
+ this.removeAllGeneGlows();
+
+ var shadow = new fabric.Shadow({
+ color: 'red',
+ blur: 30
+ });
+ var arrows = canvas.getObjects().filter(obj => obj.id == 'arrow' && geneParams.some(g => g.genomeID == obj.genomeID && g.geneID == obj.geneID));
+
+ if(indefinite) {
+ for(arrow of arrows) {
+ shadow.blur = 20;
+ arrow.set('stroke', 'black');
+ arrow.set('shadow', shadow);
+ }
+ canvas.renderAll();
+ return;
+ }
+
+ for (arrow of arrows) {
+ arrow.set('shadow', shadow);
+ arrow.animate('shadow.blur', 0, {
+ duration: timeInterval,
+ onChange: canvas.renderAll.bind(canvas),
+ onComplete: function () {},
+ easing: fabric.util.ease['easeInQuad']
+ });
+ }
+ canvas.renderAll();
+}
+
+/*
+ * Removes all active gene glow effects.
+ */
+GenomeDrawer.prototype.removeAllGeneGlows = function () {
+ canvas.getObjects().filter(obj => obj.id == 'arrow' && obj.shadow).forEach(arrow => {
+ arrow.set('stroke', 'gray');
+ delete arrow.shadow;
+ });
+ canvas.renderAll();
+}
+
+/*
+ * Shift genomes horizontally to align genes around the target gene cluster.
+ *
+ * @param gc : target gene cluster ID
+ */
+GenomeDrawer.prototype.alignToCluster = function (gc) {
+ if (!this.settings['genomeData']['gene_associations']["anvio-pangenome"]) return;
+
+ let targetGeneInfo = viewCluster(gc);
+ if (targetGeneInfo == null) return;
+ let [firstGenomeID, targetGeneMid] = targetGeneInfo;
+ if (firstGenomeID != null) {
+ alignToGC = gc;
+ let index = this.settings['genomeData']['genomes'].findIndex(g => g[0] == firstGenomeID);
+ for (var i = index + 1; i < this.settings['genomeData']['genomes'].length; i++) {
+ let gid = genomeData.genomes[i][0];
+ let geneMids = getGenePosForGenome(genomeData.genomes[i][0], alignToGC);
+ if (geneMids == null) continue;
+ let geneMid = geneMids[0]; /* TODO: implementation for multiple matching gene IDs */
+ let shift = scaleFactor * (targetGeneMid - geneMid) + (xDisps[firstGenomeID] - xDisps[gid]);
+ let objs = canvas.getObjects().filter(obj => obj.groupID == gid);
+ for (o of objs) o.left += shift;
+ xDisps[gid] += shift;
+ canvas.setViewportTransform(canvas.viewportTransform);
+
+ // clear and redraw shades
+ clearShades();
+ drawTestShades();
+ }
+ }
+}
+
+/*
+ * Clear all gene links from the canvas.
+ */
+GenomeDrawer.prototype.clearShades = function () {
+ canvas.getObjects().filter(obj => obj.id == 'link').forEach((l) => { canvas.remove(l) });
+}
+
+GenomeDrawer.prototype.setPtsPerADL = function (newResolution) {
+ if (isNaN(newResolution)) return;
+ newResolution = parseInt(newResolution);
+ if (newResolution < 0 || newResolution > genomeMax) {
+ alert(`Invalid value, genome spacing must be in range 0-${genomeMax}.`);
+ return;
+ }
+ adlPtsPerLayer = newResolution;
+ this.draw();
+}
+
+GenomeDrawer.prototype.showAllADLPts = function () {
+ this.setPtsPerADL(genomeMax);
+ $('#showAllADLPtsBtn').blur();
+}
+
+GenomeDrawer.prototype.alignRulers = function () {
+ for (genome of this.settings['genomeData']['genomes']) {
+ xDisps[genome[0]] = xDisplacement;
+ }
+ percentScale = false;
+ drawScale();
+ bindViewportToWindow();
+ updateScalePos();
+ updateRenderWindow();
+ this.draw();
+ $('#alignRulerBtn').blur();
+}
+
+GenomeDrawer.prototype.setGenomeSpacing = function (newSpacing) {
+ if (isNaN(newSpacing)) return;
+ newSpacing = parseInt(newSpacing);
+ if (newSpacing < 0 || newSpacing > 1000) {
+ alert(`Invalid value, genome spacing must be in range 0-1000.`);
+ return;
+ }
+ spacing = newSpacing;
+ this.draw();
+}
+
+GenomeDrawer.prototype.setScaleInterval = function (newScale) {
+ if (isNaN(newScale)) return;
+ newScale = parseInt(newScale);
+ if (newScale < 50) {
+ alert(`Invalid value, scale interval must be >=50.`);
+ return;
+ }
+ scaleInterval = newScale;
+ this.draw();
+}
+
+GenomeDrawer.prototype.setGeneLabelSize = function (newSize) {
+ if (isNaN(newSize)) return;
+ newSize = parseInt(newSize);
+ if (newSize < 0 || newSize > 1000) {
+ alert(`Invalid value, gene label size must be in range 0-1000.`);
+ return;
+ }
+ geneLabelSize = newSize;
+ if (showGeneLabels) this.draw();
+}
+
+GenomeDrawer.prototype.setGenomeLabelSize = function (newSize) {
+ if (isNaN(newSize)) return;
+ newSize = parseInt(newSize);
+ if (newSize < 0 || newSize > 1000) {
+ alert(`Invalid value, genome label size must be in range 0-1000.`);
+ return;
+ }
+ genomeLabelSize = newSize;
+ if (showLabels) this.draw();
+}
+
+GenomeDrawer.prototype.redrawSingleGenome = function (genomeID) {
+ canvas.getObjects().filter(o => o.groupID == genomeID).forEach(obj => canvas.remove(obj));
+ let idx = this.settings['genomeData']['genomes'].findIndex(obj => obj[0] == genomeID);
+ this.addLayers(idx);
+ checkGeneLabels();
+}
+
+/*
+ * Dynamically set scale tick interval based on scaleFactor.
+ */
+GenomeDrawer.prototype.adjustScaleInterval = function () {
+ let val = Math.floor(100 / scaleFactor);
+ let roundToDigits = Math.floor(Math.log10(val)) - 1;
+ let newInterval = Math.floor(val / (10 ** roundToDigits)) * (10 ** roundToDigits);
+ scaleInterval = newInterval;
+ $('#genome_scale_interval').val(scaleInterval);
+}
+
+GenomeDrawer.prototype.queryFunctions = async function () {
+ $('#query-results-table').empty()
+ $('#query-results-span').empty()
+ let query = $('#function_search_query').val().toLowerCase()
+ let category = $('#function_search_category').val()
+ let distinctQueryMatches = Object()
+ let glowPayload = Array()
+ let foundInGenomes = Object()
+
+ if (!query || !category) {
+ alert('please provide values for function category and/or query')
+ return
+ }
+ if (category == 'metadata'){
+ drawer.queryMetadata(query)
+ return
+ }
+ this.settings['genomeData']['genomes'].map(genome => {
+ for (const [key, value] of Object.entries(genome[1]['genes']['functions'])) {
+ if (value[category]?.[0].toLowerCase().includes(query)) {
+ let glowObject = {
+ genomeID: genome[0],
+ geneID: key,
+ matchedQuery: value[category][0]
+ }
+ glowPayload.push(glowObject)
+ if (!(genome[0] in foundInGenomes)) {
+ foundInGenomes[genome[0]] = true
+ }
+ if(!(value[category][0].toLowerCase() in distinctQueryMatches)){
+ distinctQueryMatches[value[category][0]] = true
+ }
+ } // check for accession and annotation values separately, as we want to capture the exact match for sorting results
+ else if (value[category]?.[1].toLowerCase().includes(query)) {
+ let glowObject = {
+ genomeID: genome[0],
+ geneID: key,
+ matchedQuery: value[category][1]
+ }
+ glowPayload.push(glowObject)
+ if (!(genome[0] in foundInGenomes)) {
+ foundInGenomes[genome[0]] = true
+ }
+ if(!(value[category][1].toLowerCase() in distinctQueryMatches)){
+ distinctQueryMatches[value[category][1]] = true
+ }
+ }
+ }
+ })
+
+ if (glowPayload.length < 1) {
+ alert(`No hits were found matching ${query} in ${category}`)
+ return
+ }
+ $('#query-results-span').append(` `)
+ $('#query-results-select').append(new Option('Show All', 'all'))
+ Object.keys(distinctQueryMatches).map(k => {
+ $('#query-results-select').append(new Option(k.length > 80? k.slice(0,80) + '...' : k, k))
+ })
+ $('#query-results-select').on('change', function(){
+ $('#query-results-table').empty()
+ let matchedQuery = this.value
+ if(matchedQuery == 'all'){
+ renderAllGenes()
+ } else {
+ glowPayload.filter(gene => gene['matchedQuery'] == matchedQuery).map(gene => {
+ let genomeOfInterest = settings['genomeData']['genomes'].filter(genome => genome[0] == gene['genomeID'])
+ let start = genomeOfInterest[0][1]['genes']['gene_calls'][gene['geneID']]['start']
+ let end = genomeOfInterest[0][1]['genes']['gene_calls'][gene['geneID']]['stop']
+ $('#query-results-table').append(`
+
+ ${gene['geneID']}
+ ${gene['genomeID']}
+ ${start}
+ ${end}
+ go to
+
+ `)
+ })
+ }
+ })
+ let lowestStart, highestEnd = null
+ function renderAllGenes(){
+ if(genomeMax > 35000){
+ glowPayload.map(gene => {
+ let genomeOfInterest = this.settings['genomeData']['genomes'].filter(genome => genome[0] == gene['genomeID'])
+ let start = genomeOfInterest[0][1]['genes']['gene_calls'][gene['geneID']]['start']
+ let end = genomeOfInterest[0][1]['genes']['gene_calls'][gene['geneID']]['stop']
+ if (start < lowestStart || lowestStart == null) lowestStart = start
+ if (end > highestEnd || highestEnd == null) highestEnd = end
+ $('#query-results-table').append(`
+
+ ${gene['geneID']}
+ ${gene['genomeID']}
+ ${start}
+ ${end}
+ go to
+
+ `)
+ })
+ } else {
+ lowestStart = 0
+ highestEnd = genomeMax
+ }
+ }
+ renderAllGenes()
+
+ $('#function-query-results-statement').text(`Retreived ${glowPayload.length} hit(s) from ${Object.keys(foundInGenomes).length} genomes`)
+ await zoomOutAndWait('partial', lowestStart, highestEnd, 350)
+ this.glowGenes(glowPayload, true)
+}
+
+GenomeDrawer.prototype.queryMetadata = async function(metadataLabel){
+ $('#query-results-table').empty()
+ let glowPayload = Array()
+ let foundInGenomes = Object()
+ let matches = settings['display']['metadata'].filter( m => m.label.toLowerCase().includes(metadataLabel.toLowerCase()))
+ matches.map(metadata => {
+ glowPayload.push({
+ geneID: metadata.gene,
+ genomeID: metadata.genome
+ })
+ if (!(metadata.genome in foundInGenomes)) {
+ foundInGenomes[metadata.genome] = true
+ }
+ })
+ if (glowPayload.length < 1) {
+ alert(`No hits were found matching ${metadataLabel} in metadata`)
+ return
+ }
+ let lowestStart, highestEnd = null
+ if (genomeMax > 35000) {
+ glowPayload.map(gene => {
+ let genomeOfInterest = this.settings['genomeData']['genomes'].filter(genome => genome[0] == gene['genomeID'])
+ let start = genomeOfInterest[0][1]['genes']['gene_calls'][gene['geneID']]['start']
+ let end = genomeOfInterest[0][1]['genes']['gene_calls'][gene['geneID']]['stop']
+ if (start < lowestStart || lowestStart == null) lowestStart = start
+ if (end > highestEnd || highestEnd == null) highestEnd = end
+ $('#query-results-table').append(`
+
+ ${gene['geneID']}
+ ${gene['genomeID']}
+ ${start}
+ ${end}
+ go to
+
+ `)
+ })
+ } else {
+ lowestStart = 0
+ highestEnd = genomeMax
+ }
+ await zoomOutAndWait('partial', lowestStart, highestEnd, 350)
+ this.glowGenes(glowPayload, true)
+}
+
+GenomeDrawer.prototype.setInitialZoom = function(){
+ let start = 0
+ let stop = genomeMax > 35000 ? 35000 : genomeMax
+ zoomOut('partial', start, stop)
+}
\ No newline at end of file
diff --git a/anvio/data/interactive/js/genomeview/main.js b/anvio/data/interactive/js/genomeview/main.js
new file mode 100644
index 0000000000..6547b5dc79
--- /dev/null
+++ b/anvio/data/interactive/js/genomeview/main.js
@@ -0,0 +1,337 @@
+/**
+ * Javascript library for anvi'o genome view
+ *
+ * Authors: Isaac Fink
+ * Matthew Klein
+ * A. Murat Eren
+ *
+ * Copyright 2021, The anvi'o project (http://anvio.org)
+ *
+ * Anvi'o is a free software. You can redistribute this program
+ * and/or modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation, either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with anvi'o. If not, see .
+ *
+ * @license GPL-3.0+
+ */
+
+/**
+ * File Overview : This file is the entrypoint for genomeview. Here, genomic + state data are retrieved from the backend, processed, and passed to the various other
+ * genomeview modules to build out UI and render visualizations. Generally speaking, this file should stay pretty lean and purpose-driven.
+ */
+
+var VIEWER_WIDTH = window.innerWidth || document.documentElement.clientWidth || document.getElementsByTagName('body')[0].clientWidth;
+var VIEWER_HEIGHT = window.innerHeight || document.documentElement.clientHeight || document.getElementsByTagName('body')[0].clientHeight;
+
+var current_state_name = null
+var alignToGC = null;
+var stateData = {};
+var settings = {} // packaged obj sent off to GenomeDrawer
+var xDisps = {};
+var renderWindow = [];
+var genomeMax = 0;
+var yOffset = 0 // vertical space between additional data layers
+var xDisplacement = 0; // x-offset of genome start, activated if genome labels are shown
+var scaleFactor = 1; // widths of all objects are scaled by this value to zoom in/out
+var maxGroupSize = 2 // used to calculate group height. base of 2 as each group will contain at minimum a genome layer + group ruler.
+var genomeLabelSize = 15; // font size of genome labels
+var marginTop = 20; // vertical margin at the top of the genome display
+var groupLayerPadding = 10 // padding between each layer in a given genome group
+var groupMargin = 100 // space between each genome group
+var labelSpacing = 30; // spacing default for genomeLabel canvas
+var geneLabelSize = 40; // gene label font size
+var spacing = 50; // multiplied by maxGroupSize to determine group height allocation
+var scaleInterval = 100; // nt scale intervals
+var adlPtsPerLayer = 10000; // number of data points to be subsampled per ADL. TODO: more meaningful default?
+var showLabels = true; // show genome labels?
+var showGeneLabels = true; // show gene labels?
+var link_gene_label_color_source = false // by default, allow users to display different gene arrow color / gene label source
+var dynamicScaleInterval = true; // if true, scale interval automatically adjusts to zoom level
+var percentScale = false; // if true, scale measured in proportions (0-1) of total sequence breadth rather than NT ranges.
+var geneLabelPos = "above"; // gene label position; one of "above", "inside"
+var geneLabelAngle = 0;
+var thresh_count_gene_colors = 4; // min # occurences of annotation for filtering gene color table
+var order_gene_colors_by_count = true; // if true, order annotations on gene color table by descending order of count, otherwise order alphabetically
+var filter_gene_colors_to_window = false; // if true, only display gene colors in current render window, otherwise show all gene colors in split
+var firstDraw = true // flag to determine whether to set zoom to initial zoom level
+var mainCanvasHeight;
+var canvas;
+var genomeLabelsCanvas;
+var brush;
+var drawer
+var color_db; // functional annotation type currently being used to color genes
+var counts; // stores # occurences for each category in the current function type
+var genomeData;
+
+$(document).ready(function () {
+ toastr.options = {
+ "closeButton": true,
+ "debug": false,
+ "newestOnTop": true,
+ "progressBar": false,
+ "positionClass": "toast-top-right",
+ "preventDuplicates": false,
+ "onclick": null,
+ "showDuration": "500",
+ "hideDuration": "2000",
+ "timeOut": "12000",
+ "extendedTimeOut": "1000",
+ "showEasing": "swing",
+ "hideEasing": "linear",
+ "showMethod": "fadeIn",
+ "hideMethod": "fadeOut",
+ }
+ initData();
+ loadAdditionalDataLayers()
+ processState('default', stateData)
+ loadAll('init');
+});
+
+function initData() {
+ // initialize the bulk of the data.
+ $.ajax({
+ type: 'POST',
+ cache: false,
+ url: '/data/get_genome_view_data',
+ async: false,
+ success: function (data) {
+ genomeData = data;
+ console.log("Saved the following data:");
+ console.log(data);
+
+ let genomes = Object.entries(genomeData.genomes) // an array of 2d arrays, where each genome[0] is the object key, and genome[1] is the value
+ settings['genomeData'] = genomeData
+ settings['genomeData']['genomes'] = genomes
+ }
+ });
+}
+
+function loadAdditionalDataLayers(){
+ $.ajax({
+ type: 'POST',
+ cache: false,
+ url: '/data/get_genome_view_adl',
+ async: false,
+ success: function (resp) {
+ settings['additional-data-layers'] = resp
+ settings['additional-data-layers']['layers'].push('ruler') // add ruler by default
+ settings['group-layer-order'] = ['Genome', 'Ruler']
+ settings['display'] = {}
+ settings['display']['hidden'] = {}
+ settings['display']['colors'] = {}
+ settings['display']['colors']['genes'] = {}
+ settings['display']['colors']['genes']['annotations'] = {}
+ settings['display']['colors']['Batch'] = '#FFFFFF';
+ settings['display']['layers'] = {}
+ settings['display']['labels'] = {}
+ settings['display']['labels']['set-labels'] = {}
+ settings['display']['labels']['gene-sets'] = {}
+ settings['display']['layers']['Ruler'] = true
+ settings['display']['layers']['Genome'] = true
+
+ if (settings['additional-data-layers']['layers'].includes('Coverage')) {
+ settings['group-layer-order'].unshift('Coverage')
+ settings['display']['layers']['coverage'] = true
+ settings['display']['colors']['coverage'] = '#000000'
+ maxGroupSize += 1
+ }
+
+ if (settings['additional-data-layers']['layers'].includes('GC_content')) {
+ settings['group-layer-order'].unshift('GC_Content')
+ settings['display']['layers']['GC_Content'] = true
+ settings['display']['colors']['GC_Content'] = '#000000'
+ maxGroupSize += 1
+ }
+ }
+ });
+}
+
+function loadState() {
+
+ var defer = $.Deferred();
+ $('#modLoadState').modal('hide');
+ if ($('#loadState_list').val() == null) {
+ defer.reject();
+ return;
+ }
+
+ var state_name = $('#loadState_list').val();
+
+ $.ajax({
+ type: 'GET',
+ cache: false,
+ url: '/state/get/' + state_name,
+ success: function (response) {
+ try {
+ processState(state_name, response['content']);
+ loadAll('reload')
+ } catch (e) {
+ console.error("Exception thrown", e.stack);
+ toastr.error('Failed to parse state data, ' + e);
+ defer.reject();
+ return;
+ }
+ },
+ error: function(resp){
+ console.log(resp)
+ }
+ })
+}
+
+function serializeSettings() {
+ // TODO same process as the serializeSettings() function for anvi-interactive
+ // first we run through all of the UI element default values and store them as state
+ // then we update them as necessary below in processState
+ let state = {}
+
+ state['group-layer-order'] = settings['group-layer-order']
+ state['genome-order'] = settings['genomeData']['genomes']
+ state['display'] = settings['display']
+ state['display']['bookmarks'] = settings['display']['bookmarks']
+ state['display']['metadata'] = settings['display']['metadata']
+
+ state['display']['order-method'] = $('#genome_order_select').val()
+ state['display']['dynamic-scale-interval'] = $('#show_dynamic_scale_box').is(':checked')
+ state['display']['genome-scale-interval'] = $('#genome_scale_interval').val()
+ state['display']['genome-spacing'] = $('#genome_spacing').val()
+ state['display']['arrow-style'] = $('#arrow_style').val()
+ state['display']['gene-link-style'] = $('#link_style').val()
+ state['display']['gene-shade-style'] = $('#shade_by').val()
+ state['display']['show-genome-labels'] = $('#show_genome_labels_box').is(':checked')
+ state['display']['genome-label-size'] = $('#genome_label').val()
+ state['display']['colors']['genome-label'] = $('#genome_label_color').attr(':color')
+ state['display']['show-gene-labels'] = $('#show_gene_labels_box').is(':checked')
+ state['display']['gene-label-size'] = $('#gene_label').val()
+ state['display']['colors']['gene-label'] = $('#gene_label_color').attr(':color')
+ state['display']['gene-text-position'] = $('#gene_text_pos').val()
+ state['display']['gc-window-size'] = $('#gc_window_size').val()
+ state['display']['gc-step-size'] = $('#gc_step_size').val()
+ state['display']['gc-overlay-color'] = $('#gc_overlay_color').attr(':color')
+ state['display']['gene-color-order'] = $('#gene_color_order').val()
+ state['display']['gene-label-source'] = $('#gene_label_source').val()
+ state['display']['link-gene-label-color-source'] = $('#link_gene_label_color_source').is(':checked')
+ state['display']['annotation-color-dict'] = []
+
+ $('.annotation_color').each((idx, row) => {
+ let color = $(row).attr('color')
+ let id = ($(row).attr('id').split('_')[1])
+ state['display']['annotation-color-dict'].push({
+ id : id,
+ color : color
+ })
+ })
+
+ return state
+}
+
+function processState(stateName, stateData) {
+ settings['state-name'] = stateName
+ console.log('processing this state obj', stateData)
+
+ calculateMaxGenomeLength()
+ if (stateData.hasOwnProperty('group-layer-order')) {
+ settings['group-layer-order'] = stateData['group-layer-order']
+ }
+
+ if (stateData.hasOwnProperty('additional-data-layers')) {
+ settings['additional-data-layers'] = stateData['additional-data-layers']
+ }
+
+ if (stateData.hasOwnProperty('genome-order-method')) {
+ settings['genome-order-method'] = stateData['genome-order-method']
+ settings['genome-order-method'].forEach(orderMethod => {
+ $('#genome_order_select').append((new Option(orderMethod["name"], orderMethod["name"]))) // set display + value of new select option.
+ })
+ } else {
+ generateMockGenomeOrder()
+ settings['genome-order-method'].forEach(orderMethod => {
+ $('#genome_order_select').append((new Option(orderMethod["name"], orderMethod["name"]))) // set display + value of new select option.
+ })
+ }
+
+ if (stateData.hasOwnProperty('genome-order')){
+ settings['genomeData']['genomes'] = stateData['genome-order']
+ }
+
+ if (stateData?.['display']) {
+ settings['display'] = stateData['display']
+ }
+
+ if (stateData?.['display']?.['arrow-style']){
+ $('#arrow_style').val(stateData['arrow-style'])
+ } else {
+ $('#arrow_style').val(settings['display']['arrow-style'])
+ }
+
+ if (stateData?.['display']?.['bookmarks']) {
+ settings['display']['bookmarks'].map(bookmark => {
+ $('#bookmarks-select').append((new Option(bookmark['name'], [bookmark["start"], bookmark['stop']])))
+ })
+ respondToBookmarkSelect()
+ } else {
+ settings['display']['bookmarks'] = []
+ respondToBookmarkSelect() // set listener for user bookmark selection
+ }
+
+ if (stateData?.['display']?.['link-gene-label-color-source']){
+ $('#link_gene_label_color_source').prop('checked', settings['display']['link-gene-label-color-source'])
+ }
+
+ $('#tbody_additionalDataLayers').html('') // clear div before reprocess
+ settings['group-layer-order'].map(layer => {
+ buildGroupLayersTable(layer)
+ })
+}
+
+function loadAll(loadType) {
+ canvas = new fabric.Canvas('myCanvas');
+ canvas.clear() // clear existing canvas if loadAll is being called from loadState
+ canvas.setWidth(VIEWER_WIDTH * 0.85);
+
+ $('.container').css({ 'height': VIEWER_HEIGHT + 'px', 'overflow-y': 'auto' })
+ xDisplacement = showLabels ? 120 : 0;
+ for (genome of settings['genomeData']['genomes']) {
+ xDisps[genome[0]] = xDisplacement;
+ }
+
+ calculateMaxGenomeLength()
+
+ if (showGeneLabels && parseInt(settings['display']['arrow-style']) != 3) {
+ marginTop = 60;
+ spacing = settings['display']['genome-spacing'] ? settings['display']['genome-spacing'] : 200; // TODO maybe we refactor this out into a setSpacing() method for clarity?
+ $("#genome_spacing").val(spacing);
+ }
+
+ $('#gene_color_order').append($('', {
+ value: 'Source',
+ text: 'Source'
+ }));
+ for (fn of getFunctionalAnnotations()) {
+ $('#gene_color_order').append($(' ', {
+ value: fn,
+ text: fn
+ }));
+ }
+
+ color_db = $('#gene_color_order').val();
+
+ buildGenomesTable(settings['genomeData']['genomes'], 'alphabetical') // hardcode order method until backend order data is hooked in
+ if(loadType == 'init'){
+ generateColorTable(fn_colors = null, fn_type = color_db);
+ }
+ drawScale();
+ setEventListeners()
+
+ buildGeneLabelsSelect()
+ brush.extent([parseInt($('#brush_start').val()), parseInt($('#brush_end').val())]);
+ brush(d3.select(".brush"));
+ updateRenderWindow();
+
+ console.log('Sending this data obj to GenomeDrawer', settings)
+ drawer = new GenomeDrawer(settings)
+ drawer.draw('draw from loadAll')
+ toastr.success(`Successfully loaded from ${settings['state-name']} state`)
+}
diff --git a/anvio/data/interactive/js/genomeview/navigation.js b/anvio/data/interactive/js/genomeview/navigation.js
new file mode 100644
index 0000000000..9056041bbf
--- /dev/null
+++ b/anvio/data/interactive/js/genomeview/navigation.js
@@ -0,0 +1,244 @@
+/**
+ * Javascript library for anvi'o genome view
+ *
+ * Authors: Isaac Fink
+ * Matthew Klein
+ * A. Murat Eren
+ *
+ * Copyright 2021, The anvi'o project (http://anvio.org)
+ *
+ * Anvi'o is a free software. You can redistribute this program
+ * and/or modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation, either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with anvi'o. If not, see .
+ *
+ * @license GPL-3.0+
+ */
+
+/**
+ * File Overview : This file contains code related to the functionality of genomeview's scale element and other general navigation items.
+ * Scale is great! Scale is dynamic! Scale is responsive.
+ */
+
+ function zoomIn() {
+ let start = parseInt($('#brush_start').val()), end = parseInt($('#brush_end').val());
+ let newStart, newEnd;
+
+ let len = end - start;
+ if(len > 4*genomeMax/50) {
+ newStart = Math.floor(start + genomeMax/50), newEnd = Math.floor(end - genomeMax/50);
+ } else {
+ if(len < 50) return;
+ newStart = Math.floor(start + len/4);
+ newEnd = Math.floor(end - len/4);
+ if(newEnd - newStart <= 0) return;
+ }
+
+ brush.extent([newStart, newEnd]);
+ brush(d3.select(".brush").transition());
+ brush.event(d3.select(".brush").transition());
+ }
+
+ function zoomOut(type, start, end) {
+ let newStart, newEnd
+ if(type && type == 'fully'){
+ newStart = 0
+ newEnd = genomeMax
+ } else if(type && type == 'partial'){
+ newStart = start - 10000
+ newEnd = end + 10000
+ }else {
+ let start = parseInt($('#brush_start').val()), end = parseInt($('#brush_end').val());
+ newStart = start - genomeMax/50;
+ newEnd = end + genomeMax/50;
+ if(newStart == 0 && newEnd == genomeMax) { // for extra-zoomed-out view
+ scaleFactor = 0.01;
+ if(dynamicScaleInterval) adjustScaleInterval();
+ drawer.draw()
+ return;
+ }
+ }
+ if(newStart < 0) newStart = 0;
+ if(newEnd > genomeMax) newEnd = genomeMax;
+
+ brush.extent([newStart, newEnd]);
+ brush(d3.select(".brush").transition());
+ brush.event(d3.select(".brush").transition());
+ }
+
+ async function zoomOutAndWait(type, start, end, time) {
+ zoomOut(type, start, end);
+
+ return new Promise(resolve => {
+ setTimeout(() => {
+ resolve();
+ }, time);
+ });
+ }
+
+ async function goToGene(genomeID, geneID, start, end) {
+ await zoomOutAndWait('partial', start, end, 500);
+ drawer.glowGenes([{genomeID: genomeID, geneID: geneID}]);
+ }
+
+/*
+ * Resets viewport if outside bounds of the view window, with padding on each end
+ */
+function bindViewportToWindow() {
+ let vpt = canvas.viewportTransform;
+ let [l,r] = calcXBounds();
+ if(vpt[4] > 250 - l) {
+ vpt[4] = 250 - l;
+ } else if(vpt[4] < canvas.getWidth() - r - 125) {
+ vpt[4] = canvas.getWidth() - r - 125;
+ }
+}
+
+/*
+ * Replaces nt scale with a 0-1 proportional scale
+ */
+function setPercentScale() {
+ percentScale = true;
+ drawScale();
+}
+
+function drawScale() {
+ let scaleWidth = canvas.getWidth();
+ let scaleHeight = 100;
+ let domain = percentScale ? [0,1] : [0,genomeMax];
+ let xScale = d3.scale.linear().range([0, scaleWidth]).domain(domain);
+ let scaleAxis = d3.svg.axis()
+ .scale(xScale)
+ .tickSize(scaleHeight);
+ let scaleArea = d3.svg.area()
+ .interpolate("monotone")
+ .x(function(d) { return xScale(d); })
+ .y0(scaleHeight)
+ .y1(0);
+ brush = d3.svg.brush()
+ .x(xScale)
+ .on("brushend", onBrush);
+
+ $('#scaleSvg').empty();
+ let scaleBox = d3.select("#scaleSvg").append("g")
+ .attr("id", "scaleBox")
+ .attr("class","scale")
+ .attr("y", 230)
+ .attr("transform", percentScale ? "translate(10,0)" : "translate(5,0)");
+
+ scaleBox.append("g")
+ .attr("id", "scaleMarkers")
+ .attr("class", "x axis top noselect")
+ .attr("transform", "translate(0,0)")
+ .call(scaleAxis);
+
+ scaleBox.append("g")
+ .attr("class", "x brush")
+ .call(brush)
+ .selectAll("rect")
+ .attr("y", 0)
+ .attr("height", scaleHeight);
+
+ $("#scaleSvg").attr("width", percentScale ? scaleWidth + 20 : scaleWidth + 10);
+
+ function onBrush(){
+ var b = brush.empty() ? xScale.domain() : brush.extent();
+
+ if (brush.empty()) {
+ $('.btn-selection-sequence').addClass('disabled').prop('disabled', true);
+ } else {
+ $('.btn-selection-sequence').removeClass('disabled').prop('disabled', false);
+ }
+
+ if(!percentScale) b = [Math.floor(b[0]), Math.floor(b[1])];
+
+ $('#brush_start').val(b[0]);
+ $('#brush_end').val(b[1]);
+
+ let ntsToShow = b[1] - b[0];
+ scaleFactor = percentScale ? canvas.getWidth()/(ntsToShow*genomeMax) : canvas.getWidth()/ntsToShow;
+ updateRenderWindow();
+
+ if(dynamicScaleInterval) drawer.adjustScaleInterval();
+
+ drawer.draw()
+ let moveToX = percentScale ? getRenderXRangeForFrac()[0] : xDisplacement+scaleFactor*b[0];
+ canvas.absolutePan({x: moveToX, y: 0});
+
+ // TODO: restrict min view to 300 NTs? (or e.g. scaleFactor <= 4)
+ }
+}
+
+/*
+ * Update scale info to match viewport location.
+ */
+function updateScalePos() {
+ let [newStart, newEnd] = percentScale ? getFracForVPT() : getNTRangeForVPT();
+ brush.extent([newStart, newEnd]);
+ brush(d3.select(".brush").transition());
+ $('#brush_start').val(newStart);
+ $('#brush_end').val(newEnd);
+}
+
+function updateRenderWindow() {
+ if(percentScale) {
+ let resolution = 4; // # decimals to show for renderw window
+ let [start, end] = [parseFloat($('#brush_start').val()), parseFloat($('#brush_end').val())];
+ let diff = end - start > 0.1 ? Math.round(10**resolution*(end - start) / 2)/10**resolution : 0.05;
+ renderWindow = [clamp(start - diff, 0, 1), clamp(end + diff, 0, 1)];
+ } else {
+ let [start, end] = [parseInt($('#brush_start').val()), parseInt($('#brush_end').val())];
+ let diff = end - start > 10000 ? Math.floor((end - start)/2) : 5000;
+ renderWindow = [clamp(start - diff, 0, genomeMax), clamp(end + diff, 0, genomeMax)];
+
+ if(filter_gene_colors_to_window) {
+ generateColorTable(null, color_db);
+ // TODO: filter to window for percent scale, too
+ }
+ }
+}
+
+/*
+ * Pan viewport to the first gene in the target gene cluster.
+ *
+ * @param gc : target gene cluster ID
+ * @returns tuple [a,b] where
+ * a is genomeID of the first genome containing `gc` and
+ * b is NT position of the middle of the target gene
+*/
+function viewCluster(gc) {
+ if(!genomeData.gene_associations["anvio-pangenome"]) return;
+
+ let genes = [];
+ let geneMid;
+ let first = true;
+ let firstGenomeID;
+
+ if(!gc || gc in genomeData.gene_associations["anvio-pangenome"]["gene-cluster-name-to-genomes-and-genes"]) {
+ for(genome of genomeData.genomes) {
+ var targetGenes = getGenesOfGC(genome[0], gc);
+ if(targetGenes == null) continue;
+ var targetGeneID = targetGenes[0]; /* TODO: implementation for multiple matching gene IDs */
+ var targetGene = genome[1].genes.gene_calls[targetGeneID];
+ genes.push({genomeID:genome[0],geneID:targetGeneID});
+ if(first) {
+ geneMid = targetGene.start + (targetGene.stop - targetGene.start) / 2;
+ canvas.absolutePan({x: scaleFactor*geneMid + xDisps[genome[0]] - canvas.getWidth()/2, y: 0});
+ canvas.viewportTransform[4] = clamp(canvas.viewportTransform[4], canvas.getWidth() - genomeMax*scaleFactor - xDisps[genome[0]] - 125, 125);
+ firstGenomeID = genome[0];
+ first = false;
+ }
+ }
+ updateScalePos();
+ updateRenderWindow();
+ drawer.draw();
+ drawer.glowGenes(genes, true);
+ return (first ? null : [firstGenomeID, geneMid]);
+ } else {
+ console.log('Warning: ' + gc + ' is not a gene cluster in data structure');
+ return null;
+ }
+}
diff --git a/anvio/data/interactive/js/genomeview/test.js b/anvio/data/interactive/js/genomeview/test.js
new file mode 100644
index 0000000000..a514d29518
--- /dev/null
+++ b/anvio/data/interactive/js/genomeview/test.js
@@ -0,0 +1,64 @@
+/**
+ * Javascript library for anvi'o genome view
+ *
+ * Authors: Isaac Fink
+ * Matthew Klein
+ * A. Murat Eren
+ *
+ * Copyright 2021, The anvi'o project (http://anvio.org)
+ *
+ * Anvi'o is a free software. You can redistribute this program
+ * and/or modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation, either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with anvi'o. If not, see .
+ *
+ * @license GPL-3.0+
+ */
+
+/**
+ * File Overview : This file contains temporary functions and processes used to test various functionality within
+ * genomeview. They are gathered here so as not to pollute the other genomeview files.
+ */
+
+function drawTestShades() {
+ drawer.shadeGeneClusters(["GC_00000034", "GC_00000097", "GC_00000002"], { "GC_00000034": "green", "GC_00000097": "red", "GC_00000002": "purple" });
+}
+
+function generateMockADL() {
+ settings['mock-additional-data-layers'] = []
+
+ for (let i = 0; i < settings['genomeData']['genomes'].length; i++) { // generate mock additional data layer content
+ let gcContent = []
+ let coverage = []
+ for (let j = 0; j < genomeMax; j++) {
+ gcContent.push(Math.floor(Math.random() * 45))
+ coverage.push(Math.floor(Math.random() * 45))
+ }
+
+ let genomeLabel = Object.keys(settings['genomeData']['genomes'][i][1]['contigs']['info'])[0];
+ let additionalDataObject = {
+ 'genome': genomeLabel,
+ 'coverage': coverage,
+ 'coverage-color': '#05cde3',
+ 'gcContent': gcContent,
+ 'gcContent-color': '#9b07e0',
+ 'ruler': true // TODO: store any genome-specific scale data here
+ }
+ settings['mock-additional-data-layers'].push(additionalDataObject)
+ }
+}
+function generateMockGenomeOrder() {
+ settings['genome-order-method'] = [{
+ 'name': 'cats',
+ 'ordering': 'some order'
+ }, {
+ 'name': 'dogs',
+ 'ordering': 'some other order'
+ }, {
+ 'name': 'birds',
+ 'ordering': 'beaks to tails'
+ }]
+}
\ No newline at end of file
diff --git a/anvio/data/interactive/js/genomeview/ui.js b/anvio/data/interactive/js/genomeview/ui.js
new file mode 100644
index 0000000000..973a105c03
--- /dev/null
+++ b/anvio/data/interactive/js/genomeview/ui.js
@@ -0,0 +1,1443 @@
+/**
+ * Javascript library for anvi'o genome view
+ *
+ * Authors: Isaac Fink
+ * Matthew Klein
+ * A. Murat Eren
+ *
+ * Copyright 2021, The anvi'o project (http://anvio.org)
+ *
+ * Anvi'o is a free software. You can redistribute this program
+ * and/or modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation, either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with anvi'o. If not, see .
+ *
+ * @license GPL-3.0+
+ */
+
+/**
+ * File Overview : This file contains functions related to building + updating UI elements and responding to user interaction with those elements.
+ * As a general rule, processes that invoke jQuery should probably live here.
+ */
+
+/*
+ * set event listeners for DOM elements, user input, default jquery values
+ */
+function setEventListeners(){
+ canvas.on('mouse:over', (event) => {
+ if (event.target && event.target.id === 'arrow') {
+ showToolTip(event)
+ }
+ })
+
+ // panning
+ canvas.on('mouse:down', function (opt) {
+ var evt = opt.e;
+ if (evt.shiftKey === true) {
+ this.isDragging = true;
+ this.selection = false;
+ this.lastPosX = evt.clientX;
+ }
+ this.shades = true;
+ if (opt.target && opt.target.groupID) this.prev = opt.target.left;
+ });
+ canvas.on('mouse:move', function (opt) {
+ if (this.isDragging) {
+ var e = opt.e;
+ var vpt = this.viewportTransform;
+ vpt[4] += e.clientX - this.lastPosX;
+ bindViewportToWindow();
+ this.requestRenderAll();
+ this.lastPosX = e.clientX;
+
+ let [l, r] = percentScale ? getFracForVPT() : getNTRangeForVPT();
+ if (l < renderWindow[0] || r > renderWindow[1]) {
+ updateRenderWindow();
+ drawer.draw()
+ }
+ }
+ });
+ canvas.on('mouse:up', function (opt) {
+ this.setViewportTransform(this.viewportTransform);
+ if (this.isDragging) updateScalePos();
+ this.isDragging = false;
+ this.selection = true;
+ if (!this.shades) {
+ // slide a genome
+ this.shades = true;
+ drawTestShades();
+ bindViewportToWindow();
+ updateScalePos(); // adjust scale box to new sequence breadth
+ updateRenderWindow();
+ drawer.redrawSingleGenome(opt.target.groupID);
+ }
+ });
+ canvas.on('object:moving', function (opt) {
+ var gid = opt.target ? opt.target.groupID : null;
+ if (gid == null) return;
+
+ if (opt.target.id == 'genomeLine' || (opt.target.id == 'arrow' && settings['display']?.['arrowStyle'] == 3)) canvas.sendBackwards(opt.target);
+ if (this.shades) {
+ drawer.clearShades();
+ this.shades = false;
+ }
+
+ let objs = canvas.getObjects().filter(obj => obj.groupID == gid);
+
+ var delta = opt.target.left - this.prev;
+ canvas.getObjects().filter(obj => obj.groupID == gid).forEach(o => {
+ if (o !== opt.target) o.left += delta;
+ });
+ xDisps[gid] += delta;
+
+ this.setViewportTransform(this.viewportTransform);
+ setPercentScale();
+ this.prev = opt.target.left;
+ });
+ canvas.on('mouse:wheel', function (opt) {
+ opt.e.preventDefault();
+ opt.e.stopPropagation();
+
+ var delta = opt.e.deltaY;
+ let tmp = scaleFactor * (0.999 ** delta);
+ let diff = tmp - scaleFactor;
+ let [start, end] = [parseInt($('#brush_start').val()), parseInt($('#brush_end').val())];
+ let [newStart, newEnd] = [Math.floor(start - diff * genomeMax), Math.floor(end + diff * genomeMax)];
+ if (newStart < 0) newStart = 0;
+ if (newEnd > genomeMax) newEnd = genomeMax;
+ if (newEnd - newStart < 50) return;
+
+ brush.extent([newStart, newEnd]);
+ brush(d3.select(".brush").transition()); // if zoom is slow or choppy, try removing .transition()
+ brush.event(d3.select(".brush"));
+ $('#brush_start').val(newStart);
+ $('#brush_end').val(newEnd);
+ });
+ canvas.on('mouse:over', function(event) {
+ // if(event.target.class == 'ruler'){
+ // console.log(event.target)
+ // }
+ })
+
+ $('#alignClusterInput').on('keydown', function (e) {
+ if (e.keyCode == 13) { // 13 = enter key
+ drawer.alignToCluster($(this).val());
+ $(this).blur();
+ }
+ });
+ $('#panClusterInput').on('keydown', function (e) {
+ if (e.keyCode == 13) { // 13 = enter key
+ viewCluster($(this).val());
+ $(this).blur();
+ }
+ });
+ document.body.addEventListener("keydown", function (ev) {
+ if (ev.which == 83 && ev.target.nodeName !== 'TEXTAREA' && ev.target.nodeName !== 'INPUT') { // S = 83
+ toggleSettingsPanel();
+ }
+ });
+ document.body.addEventListener("keydown", function (ev) {
+ if (ev.which == 77 && ev.target.nodeName !== 'TEXTAREA' && ev.target.nodeName !== 'INPUT') { // M = 77
+ toggleRightPanel('#mouseover-panel')
+ }
+ });
+ document.body.addEventListener("keydown", function (ev) {
+ if (ev.which == 81 && ev.target.nodeName !== 'TEXTAREA' && ev.target.nodeName !== 'INPUT') { // Q = 81
+ toggleRightPanel('#query-panel')
+ }
+ });
+ $('#genome_spacing').on('keydown', function (e) {
+ if (e.keyCode == 13) { // 13 = enter key
+ drawer.setGenomeSpacing($(this).val());
+ $(this).blur();
+ }
+ });
+ $('#gene_label').on('keydown', function (e) {
+ if (e.keyCode == 13) { // 13 = enter key
+ drawer.setGeneLabelSize($(this).val());
+ $(this).blur();
+ }
+ });
+ $('#genome_label').on('keydown', function (e) {
+ if (e.keyCode == 13) { // 13 = enter key
+ drawer.setGenomeLabelSize($(this).val());
+ $(this).blur();
+ }
+ });
+ $('#genome_scale_interval').on('keydown', function (e) {
+ if (e.keyCode == 13) { // 13 = enter key
+ drawer.setScaleInterval($(this).val());
+ $(this).blur();
+ }
+ });
+ $('#gene_color_order').on('change', function () {
+ color_db = $(this).val();
+ generateColorTable(settings.display.colors.genes.annotations[color_db], color_db); // TODO: include highlight_genes, fn_colors etc from state
+
+ if(link_gene_label_color_source){
+ $('#gene_label_source').val($(this).val())
+ }
+
+ drawer.draw()
+ $(this).blur();
+ });
+ $('#arrow_style').on('change', function () {
+ // arrowStyle = parseInt($(this).val());
+ settings['display']['arrow-style'] = parseInt($(this).val());
+ drawer.draw()
+ $(this).blur();
+ });
+ $('#gene_text_pos').on('change', function () {
+ geneLabelPos = $(this).val();
+ if (!(geneLabelPos == "inside" && settings['display']['arrow-style'] != 3)) drawer.draw();
+ $(this).blur();
+ });
+ $('#gene_text_angle').on('change', function () {
+ geneLabelAngle = $(this).val();
+ if (geneLabelPos != "inside") drawer.draw();
+ $(this).blur();
+ });
+ $('#show_genome_labels_box').on('change', function () {
+ showLabels = !showLabels;
+ xDisplacement = showLabels ? 120 : 0;
+ alignToGC = null;
+ drawer.draw()
+ });
+ $('#gene_label_source').on('change', function(){
+ if(link_gene_label_color_source){
+ color_db = $(this).val();
+ $('#gene_color_order').val($(this).val())
+ generateColorTable(settings.display.colors.genes.annotations[color_db], color_db)
+ }
+ drawer.draw()
+ })
+ $('#show_gene_labels_box').on('change', function () {
+ showGeneLabels = !showGeneLabels;
+ drawer.draw()
+ });
+ $('#link_gene_label_color_source').on('change', function(){
+ link_gene_label_color_source = !link_gene_label_color_source;
+ drawer.draw()
+ })
+ $('#show_only_cags_in_window').on('change', function () {
+ filter_gene_colors_to_window = !filter_gene_colors_to_window;
+ generateColorTable(settings.display.colors.genes.annotations[color_db], color_db);
+ });
+ $('#thresh_count').on('keydown', function (e) {
+ if (e.keyCode == 13) { // 13 = enter key
+ filterColorTable($(this).val());
+ $(this).blur();
+ }
+ });
+ $('#show_dynamic_scale_box').on('change', function () {
+ dynamicScaleInterval = !dynamicScaleInterval;
+ });
+ $('#adl_pts_per_layer').on('change', function () {
+ drawer.setPtsPerADL($(this).val());
+ $(this).blur();
+ });
+ $('#brush_start, #brush_end').keydown(function (ev) {
+ if (ev.which == 13) { // enter key
+ let [start, end] = percentScale ? [parseFloat($('#brush_start').val()), parseFloat($('#brush_end').val())]
+ : [parseInt($('#brush_start').val()), parseInt($('#brush_end').val())];
+ let endBound = percentScale ? 1 : genomeMax;
+
+ if (isNaN(start) || isNaN(end) || start < 0 || start > endBound || end < 0 || end > endBound) {
+ alert(`Invalid value, value needs to be in range 0-${endBound}.`);
+ return;
+ }
+
+ if (start >= end) {
+ alert('Starting value cannot be greater or equal to the ending value.');
+ return;
+ }
+
+ brush.extent([start, end]);
+ brush(d3.select(".brush").transition());
+ brush.event(d3.select(".brush").transition());
+ }
+ });
+ $('#batch_colorpicker').colpick({
+ layout: 'hex',
+ submit: 0,
+ colorScheme: 'light',
+ onChange: function(hsb, hex, rgb, el, bySetColor) {
+ $(el).css('background-color', '#' + hex);
+ $(el).attr('color', '#' + hex);
+ settings['display']['colors']['Batch'] = '#' + hex;
+ if (!bySetColor) $(el).val(hex);
+ }
+ }).keyup(function() {
+ $(this).colpickSetColor(this.value);
+ });
+ $('#genome_label_color').colpick({
+ layout: 'hex',
+ submit: 0,
+ colorScheme: 'light',
+ onChange: function(hsb, hex, rgb, el, bySetColor) {
+ $(el).css('background-color', '#' + hex);
+ $(el).attr('color', '#' + hex);
+ if (!bySetColor) $(el).val(hex);
+ }
+ }).keyup(function() {
+ $(this).colpickSetColor(this.value);
+ });
+ $('#gene_label_color').colpick({
+ layout: 'hex',
+ submit: 0,
+ colorScheme: 'light',
+ onChange: function(hsb, hex, rgb, el, bySetColor) {
+ $(el).css('background-color', '#' + hex);
+ $(el).attr('color', '#' + hex);
+ if (!bySetColor) $(el).val(hex);
+ }
+ }).keyup(function() {
+ $(this).colpickSetColor(this.value);
+ });
+
+ $('#brush_start').val(0);
+ $('#brush_end').val(Math.floor(canvas.getWidth()));
+
+ $('#deepdive-tooltip-body').hide() // set initual tooltip hide value
+ $('#tooltip-body').hide()
+ $('#show_genome_labels_box').attr("checked", showLabels);
+ $('#show_gene_labels_box').attr("checked", showGeneLabels);
+ $('#show_dynamic_scale_box').attr("checked", dynamicScaleInterval);
+
+ $("#tabular-modal-body").on('shown.bs.modal', function(){
+ showTabularModal()
+ });
+ $("#tabular-modal-body").on('hide.bs.modal', function(){
+ $('#tabular-modal-nav-tabs').html('')
+ $('#modal-tab-content').html('')
+ });
+
+ // can either set arrow click listener on the canvas to check for all arrows, or when arrow is created.
+
+ // drag box selection
+ canvas.on('selection:created', (e) => {
+ let selected_genes = e.selected.filter(obj => obj.id == 'arrow');
+
+ if(selected_genes.length > 1) {
+ showLassoMenu(selected_genes, e.e.clientX, e.e.clientY);
+ }
+
+ // disable group selection
+ if (e.target.type === 'activeSelection') {
+ canvas.discardActiveObject();
+ }
+ })
+
+ canvas.on('mouse:down', (event) => {
+ if(event.target && event.target.id === 'arrow'){
+ showDeepDiveToolTip(event)
+ }
+ $('#lasso-modal-body').modal('hide')
+ })
+}
+function showDeepDiveToolTip(event){
+ $('.canvas-container').dblclick(function(){
+ if($("#deepdive-tooltip-body").is(":visible")){
+ $("#deepdive-tooltip-body").hide()
+ }
+ })
+
+ $('#deepdive-modal-tab-content').html('')
+ let totalMetadataString = String()
+ let totalAnnotationsString = String()
+ let metadataLabel = String()
+ let button = `Query Sequence for matches `
+
+ if(settings['display']?.['metadata']){
+ let geneMetadata = settings['display']['metadata'].filter(metadata => metadata.genome == event.target.genomeID && metadata.gene == event.target.geneID )
+ const createMetadataContent = () => {
+ geneMetadata.map(metadata => {
+ metadataLabel = metadata.label
+
+ totalMetadataString += `
+
+ ${metadata.label}
+ ${metadata.type == 'tag'? button : 'n/a'}
+
+ `
+ })
+ }
+ createMetadataContent()
+ } else { // create metadata array on first tooltip load if none exists
+ settings['display']['metadata'] = []
+ }
+
+ if(event.target.functions){
+ Object.entries(event.target.functions).map(func => {
+ totalAnnotationsString += `
+
+ ${func[0]}
+ ${func[1] ? func[1][0] : 'n/a'}
+ ${func?.[1]?.[1]}
+
+ `
+ })
+ }
+
+ $('#deepdive-modal-body').modal('show')
+ $('#deepdive-modal-tab-content').append(`
+
+ Gene Call
+
+
+ ID
+ Source
+ Length
+ Direction
+ Start
+ Stop
+ Call type
+
+
+
+ ${event.target.geneID}
+ ${event.target.gene?.source}
+ ${event.target.gene.stop - event.target.gene.start}
+ ${event.target.gene.direction}
+ ${event.target.gene.start}
+ ${event.target.gene.stop}
+ ${event.target.gene?.call_type}
+
+
+
;
+
+ DNA
+ AA
+ blastn @ nr
+ blastn @ refseq_genomic
+ blastx @ nr
+ blastx @ refseq_genomic
+
+
+ Visible Geen Range
+ Downstream Visible Gene Range
+
+ Upstream Visible Gene Range
+
+
+ Set Visible Range
+
+
+ color
+
+ set gene arrow color
+
+ metadata
+
+
+ Annotations ;
+ ;
+
+ Source ;
+ Accession ;
+ Annotation
+ ;
+ ;
+ ${totalAnnotationsString}
+
+
;
+ `)
+
+ $('#metadata-query').on('click', function(){
+ drawer.queryMetadata(metadataLabel)
+ })
+
+ $('#gene-visibility-range-set').click(function(){
+ setGeneVisibilityRange(event.target.geneID, event.target.genomeID)
+ })
+
+ $('#gene-dna-sequence-button').on('click', function(){
+ show_sequence_modal('DNA Sequence', settings['genomeData']['genomes'].filter(genome => genome[0] == event.target.genomeID)[0][1]['genes']['dna'][event.target.geneID], event.target.geneID, event.target.genomeID)
+ })
+
+ $('#gene-aa-sequence-button').on('click', function(){
+ show_sequence_modal('AA Sequence', settings['genomeData']['genomes'].filter(genome => genome[0] == event.target.genomeID)[0][1]['genes']['aa'][event.target.geneID], event.target.geneID, event.target.genomeID)
+ })
+
+ $('#gene-blastx-at-nr-button').on('click', function(){
+ let sequence = settings['genomeData']['genomes'].filter(genome => genome[0] == event.target.genomeID)[0][1]['genes']['dna'][event.target.geneID]['sequence']
+ let sequenceConcat = '>' + 'DNA_SEQUENCE' + '\n' + sequence
+ fire_up_ncbi_blast(sequenceConcat, 'blastx', 'nr', 'gene')
+ })
+ $('#gene-blastn-at-nr-button').on('click', function(){
+ let sequence = settings['genomeData']['genomes'].filter(genome => genome[0] == event.target.genomeID)[0][1]['genes']['dna'][event.target.geneID]['sequence']
+ let sequenceConcat = '>' + 'DNA_SEQUENCE' + '\n' + sequence
+ fire_up_ncbi_blast(sequenceConcat, 'blastn', 'nr', 'gene')
+ })
+
+ $('#gene-blastx-at-refseq-button').on('click', function(){
+ let sequence = settings['genomeData']['genomes'].filter(genome => genome[0] == event.target.genomeID)[0][1]['genes']['dna'][event.target.geneID]['sequence']
+ let sequenceConcat = '>' + 'DNA_SEQUENCE' + '\n' + sequence
+ fire_up_ncbi_blast(sequenceConcat, 'blastx', 'refseq_genomic', 'gene')
+ })
+ $('#gene-blastn-at-refseq-button').on('click', function(){
+ let sequence = settings['genomeData']['genomes'].filter(genome => genome[0] == event.target.genomeID)[0][1]['genes']['dna'][event.target.geneID]['sequence']
+ let sequenceConcat = '>' + 'DNA_SEQUENCE' + '\n' + sequence
+ fire_up_ncbi_blast(sequenceConcat, 'blastn', 'refseq_genomic', 'gene')
+ })
+ // TODO consider metadata option to include 'author' field
+ $('#metadata-gene-label-add').on('click', function(){
+ let metadataObj = {
+ label : $('#metadata-gene-label').val(),
+ genome : event.target.genomeID,
+ gene : event.target.geneID,
+ type : 'tag'
+ }
+ settings['display']['metadata'].push(metadataObj)
+ $('#metadata-gene-label').val('')
+ $('#metadata-body').append(`
+
+ ${metadataObj.label}
+ ${button}
+
+ `)
+ $('#metadata-query').on('click', function(){ // re-trigger listener for new DOM buttons
+ drawer.queryMetadata(metadataLabel)
+ })
+ })
+ $('#metadata-gene-description-add').on('click', function(){
+ let metadataObj = {
+ label : $('#metadata-gene-description').val(),
+ genome : event.target.genomeID,
+ gene : event.target.geneID,
+ type : 'description'
+ }
+ settings['display']['metadata'].push(metadataObj)
+ $('#metadata-gene-description').val('')
+ $('#metadata-body').append(`
+
+ ${metadataObj.label}
+ n/a
+
+ `)
+ $('#metadata-query').on('click', function(){
+ drawer.queryMetadata(metadataLabel)
+ })
+ })
+ $('#picker_tooltip').colpick({
+ layout: 'hex',
+ submit: 0,
+ colorScheme: 'light',
+ onChange: function(hsb, hex, rgb, el, bySetColor) {
+ $(el).css('background-color', '#' + hex);
+ $(el).attr('color', '#' + hex);
+ if (!bySetColor) $(el).val(hex);
+ let gene = canvas.getObjects().find(obj => obj.id == 'arrow' &&
+ event.target.genomeID == obj.genomeID &&
+ event.target.geneID == obj.geneID);
+ gene.fill = `#${hex}`
+ gene.dirty = true
+ if(!settings['display']['colors']['genes']?.[event.target.genomeID]?.[event.target.geneID]){
+ if(!settings['display']['colors']['genes'][event.target.genomeID]) {
+ settings['display']['colors']['genes'][event.target.genomeID] = [];
+ }
+ settings['display']['colors']['genes'][event.target.genomeID][event.target.geneID] = `#${hex}`
+ } else {
+ settings['display']['colors']['genes'][event.target.genomeID][event.target.geneID] = `#${hex}`
+ }
+ canvas.renderAll()
+ }
+ }).keyup(function() {
+ $(this).colpickSetColor(this.value);
+ });
+}
+
+function showToolTip(event){
+ $('#mouseover-panel-table-body').html('')
+ $('#mouseover-panel-annotations-table-body').html('')
+ let totalAnnotationsString = ''
+ if(event.target.functions){
+ Object.entries(event.target.functions).map(func => {
+ totalAnnotationsString += `
+
+ ${func[0]}
+ ${func[1] ? func[1][0] : 'n/a'}
+ ${func?.[1]?.[1]}
+
+ `
+ })
+ }
+ $('#mouseover-panel-table-body').append(`
+
+ ${event.target.geneID}
+ ${event.target.gene?.source}
+ ${event.target.gene.stop - event.target.gene.start}
+ ${event.target.gene.direction}
+ ${event.target.gene.start}
+ ${event.target.gene.stop}
+ ${event.target.gene?.call_type}
+
+ `)
+ $('#mouseover-panel-annotations-table-body').append(totalAnnotationsString)
+}
+
+function show_sequence_modal(title, gene, geneID, genomeID) {
+ // remove previous modal window
+ $('.modal-sequence').modal('hide');
+ $('.modal-sequence').remove();
+
+ let header, content
+ if(title == 'DNA Sequence'){
+ header = `>${geneID}|contig:${gene['contig']}|start:${gene['start']}|stop:${gene['stop']}|direction:${gene['direction']}|rev_compd:${gene['rev_compd']}|length:${gene['length']}`
+ content = header + '\n' + gene['sequence']
+ } else if (title == 'AA Sequence' ){
+ // get header data from equivalent gene dna object, as gene aa object only contains sequence
+ let dna_gene_obj = settings['genomeData']['genomes'].filter(g => g[0] == genomeID)[0][1]['genes']['dna'][geneID]
+ header = `>${geneID}|contig:${dna_gene_obj['contig']}|start:${dna_gene_obj['start']}|stop:${dna_gene_obj['stop']}|direction:${dna_gene_obj['direction']}|rev_compd:${dna_gene_obj['rev_compd']}|length:${dna_gene_obj['length']}`
+ content = header + '\n' + gene['sequence']
+ }
+
+ $('body').append('');
+ $('.modal-sequence').modal('show');
+ $('.modal-sequence textarea').trigger('click');
+}
+
+function showTabularModal(){
+ var arrows = canvas.getObjects().filter(obj => obj.id == 'arrow')
+ var genomesObj = Object()
+ var functionSourcesArr = Array()
+
+ arrows.map(arrow => {
+ if(!genomesObj[arrow['genomeID']]){
+ genomesObj[arrow['genomeID']] = [arrow]
+ } else {
+ genomesObj[arrow['genomeID']].push(arrow)
+ }
+ if(arrow['functions']){
+ Object.keys(arrow['functions']).forEach(source => {
+ if(!functionSourcesArr.includes(source)){
+ functionSourcesArr.push(source)
+ }
+ })
+ }
+ })
+
+ handleSequenceSourceSelect = (target, type) => {
+ if($(target).is(':checked')){
+ Object.entries(genomesObj).map(genome => {
+ $(`#${genome[0]}-tabular-modal-table-header-tr`).append(`${type} `)
+ genome[1].forEach(gene => {
+ if(type == 'dna'){
+ $(`#${genome[0]}-table-row-${gene['geneID']}`).append(`${gene['dnaSequence']} `)
+ } else if(type == 'aa'){
+ $(`#${genome[0]}-table-row-${gene['geneID']}`).append(`${gene['aaSequence']} `)
+ }
+ })
+ })
+ } else {
+ Object.entries(genomesObj).map(genome => {
+ $(`#th-source-${genome[0]}-${type}`).remove()
+ genome[1].forEach(gene => {
+ $(`#${genome[0]}-${gene['geneID']}-sequence`).remove()
+ })
+ })
+
+ }
+ }
+
+ handleAnnotationSourceSelect = (target) => {
+ if($(target).is(":checked")){
+ Object.entries(genomesObj).map(genome => {
+ $(`#${genome[0]}-tabular-modal-table-header-tr`).append(`${target.value} `)
+ genome[1].forEach(gene => {
+ if(gene?.['functions']?.[target.value]){
+ $(`#${genome[0]}-table-row-${gene['geneID']}`).append(`${gene['functions'][target.value][1]} `)
+ } else {
+ $(`#${genome[0]}-table-row-${gene['geneID']}`).append(`n/a `)
+ }
+ })
+ })
+ } else {
+ Object.entries(genomesObj).map(genome => {
+ $(`#th-source-${genome[0]}-${target.value}`).remove()
+ genome[1].forEach(gene => {
+ $(`#${genome[0]}-${gene['geneID']}-${target.value}`).remove()
+ })
+ })
+ }
+ }
+
+ handleGeneShowHideToggle = (target) => {
+ let [genomeID, geneID] = target.value.split('-')
+ let arrow = canvas.getObjects().filter(obj => obj.id == 'arrow').find(arrow => arrow.geneID == geneID && arrow.genomeID == genomeID)
+ if($(target).is(':checked')){
+ if(genomeID in settings['display']['hidden']){
+ settings['display']['hidden'][genomeID][geneID] = true
+ } else {
+ settings['display']['hidden'][genomeID] = {}
+ settings['display']['hidden'][genomeID][geneID] = true
+ }
+ arrow.opacity = 0.1
+ } else {
+ delete settings['display']['hidden'][genomeID][geneID]
+ arrow.opacity = 1
+ }
+ // re-render arrow objects with updated opacity values
+ arrow.dirty = true
+ canvas.renderAll()
+ }
+
+ $('#tabular-modal-annotation-checkboxes').empty()
+ functionSourcesArr.map(s => {
+ $('#tabular-modal-annotation-checkboxes').append(
+ `
+
+ ${s}
+ `
+ )
+ })
+
+ $('#tabular-modal-sequence-checkboxes').empty()
+ $('#tabular-modal-sequence-checkboxes').append(
+ `
+ DNA Sequence
+
+
+ AA Sequence
+
+
+ `
+ )
+
+ Object.keys(genomesObj).map((genome, idx) => {
+ $('#tabular-modal-nav-tabs').append(`
+ ${genome}
+ `)
+ $('#modal-tab-content').append(`
+
+ `)
+ })
+ Object.entries(genomesObj).map((genome, idx) => {
+ let totalTableString = String()
+ let totalAnnotationsString = String()
+ genome[1].forEach(gene => {
+ if(gene['functions']){
+ Object.entries(gene['functions']).forEach((func, idx) => {
+ totalAnnotationsString += `
+
+ ${func[1] ? func[1][0].slice(0,20) : 'n/a'} || ${func?.[1]?.[1].slice(0,20)} ▼
+ ${func?.[1]?.[0]}|| ${func?.[1]?.[1]}
+
+ `
+ })
+ }
+ let geneHex
+ if(settings['display']['colors']['genes']?.[genome[0]]?.[gene['geneID']]){
+ geneHex = settings['display']['colors']['genes']?.[genome[0]]?.[gene['geneID']]
+ }
+ totalTableString += `
+
+
+ ${gene['geneID']}
+ ${gene['gene']['start']}
+ ${gene['gene']['stop']}
+ ${gene['gene']['direction']}
+ ${gene['gene']['contig']}
+ Deep Dive
+
+
+
+ `
+ })
+ $(`#${genome[0]}-table-body`).append(totalTableString)
+ })
+
+ $('.colorpicker').colpick({
+ layout: 'hex',
+ submit: 0,
+ colorScheme: 'light',
+ onChange: function(hsb, hex, rgb, el, bySetColor) {
+ $(el).css('background-color', '#' + hex);
+ $(el).attr('color', '#' + hex);
+ if (!bySetColor) $(el).val(hex);
+
+ let [geneID, genomeID] = [el.id.split('-')[0], el.id.split('-')[1]]
+
+ if(settings['display']['colors']['genes']?.[genomeID]?.[geneID]){
+ settings['display']['colors']['genes'][genomeID][geneID] = '#' + hex
+ } else {
+ settings['display']['colors']['genes'][genomeID] = {}
+ settings['display']['colors']['genes'][genomeID][geneID] = '#' + hex
+ }
+
+ let arrow = canvas.getObjects().filter(obj => obj.id == 'arrow').find(arrow => arrow.geneID == geneID && arrow.genomeID == genomeID)
+ arrow.fill = '#' + hex
+ arrow.dirty = true
+ canvas.renderAll();
+ }
+ }).keyup(function() {
+ $(this).colpickSetColor(this.value);
+ });
+}
+
+function gatherTabularModalSelectedItems(action){
+ let targetedGenes = []
+ $('#modal-tab-content :checked').each(function(){
+ let [genome, gene] = $(this).val().split('-')
+ targetedGenes.push({genomeID: genome, geneID: gene})
+ })
+
+ switch (action) {
+ case 'glow':
+ drawer.glowGenes(targetedGenes)
+ $('#tabular-modal-body').fadeOut(1000).delay(3000).fadeIn(1000)
+ break;
+ case 'color':
+ break;
+ case 'metadata':
+ break;
+ default:
+ break;
+ }
+}
+
+function transitionTabularModalToDeepdive(event){
+ let [genomeID, geneID] = event.target.id.split('-')
+ let genomeOfInterest = this.settings['genomeData']['genomes'].filter(genome => genome[0] == genomeID)
+
+ // this object generation is to mimick the expected behavior of a click event that the deepdive tooltip expects
+ let generatedEventObj = {}
+ generatedEventObj.target = {}
+ generatedEventObj.target.gene = genomeOfInterest[0][1]['genes']['dna'][geneID]
+ generatedEventObj.target.functions = genomeOfInterest[0][1]['genes']['functions'][geneID]
+ $('#tabular-modal-body').modal('hide')
+ showDeepDiveToolTip(generatedEventObj)
+}
+
+function exportTabularModalToTSV(){
+
+ // TODO function to strip out UI columns before processing
+ let titles = new Array;
+ let data = new Array;
+ let active = $("div#modal-tab-content div.active")[0].id
+
+ $(`#${active} th`).each(function() {
+ if(['Select', 'Color', 'Hidden?', 'Deepdive'].includes($(this).text())){
+ return
+ }
+ titles.push($(this).text());
+ });
+
+ $(`#${active} td`).each(function() {
+ if(['select', 'color', 'hidden?', 'deepdive'].includes($(this).attr('class'))){
+ return
+ }
+ data.push($(this).text());
+ });
+
+ let CSVString = prepTSVRow(titles, titles.length, '');
+ CSVString = prepTSVRow(data, titles.length, CSVString);
+ let downloadLink = document.createElement("a");
+ let blob = new Blob(["\ufeff", CSVString]);
+ let url = URL.createObjectURL(blob);
+ downloadLink.href = url;
+ downloadLink.download = `${active}-tabular-modal-table.tsv`;
+ downloadLink.click()
+}
+
+function prepTSVRow(arr, columnCount, initial) { // https://stackoverflow.com/questions/40428850/how-to-export-data-from-table-to-csv-file-using-jquery
+ var row = ''; // this will hold data
+ var delimeter = '\t'; // data slice separator, in excel it's `;`, in usual CSv it's `,`
+ var newLine = '\r\n'; // newline separator for CSV row
+
+ /*
+ * Convert [1,2,3,4] into [[1,2], [3,4]] while count is 2
+ * @param _arr {Array} - the actual array to split
+ * @param _count {Number} - the amount to split
+ * return {Array} - splitted array
+ */
+ function splitArray(_arr, _count) {
+ var splitted = [];
+ var result = [];
+ _arr.forEach(function(item, idx) {
+ if ((idx + 1) % _count === 0) {
+ splitted.push(item);
+ result.push(splitted);
+ splitted = [];
+ } else {
+ splitted.push(item);
+ }
+ });
+ return result;
+ }
+ var plainArr = splitArray(arr, columnCount);
+ plainArr.forEach(function(arrItem) {
+ arrItem.forEach(function(item, idx) {
+ row += item + ((idx + 1) === arrItem.length ? '' : delimeter);
+ });
+ row += newLine;
+ });
+ return initial + row;
+}
+
+function showLassoMenu(selected_genes, x, y) {
+ //x = 600;
+ //y = 200;
+ let start, stop, length;
+ let showSetLabel = false;
+ if(selected_genes.every(obj => obj.genomeID == selected_genes[0].genomeID)) {
+ start = selected_genes[0].gene.start;
+ stop = selected_genes[selected_genes.length-1].gene.stop;
+ length = stop - start;
+ showSetLabel = true;
+ } else {
+ start = stop = length = "N/A";
+ }
+ $('#lasso-modal-body').modal('show')
+ $('#lasso-modal-content').empty().append(
+ `
+ \
+ Start ${start} \
+ Stop ${stop} \
+ Length ${length} \
+ Gene count ${selected_genes.length} \
+
';
+
+
+
+
+
+ Apply
+ `
+ )
+
+ $('#picker_lasso').colpick({
+ layout: 'hex',
+ submit: 0,
+ colorScheme: 'light',
+ onChange: function(hsb, hex, rgb, el, bySetColor) {
+ $(el).css('background-color', '#' + hex);
+ $(el).attr('color', '#' + hex);
+ if (!bySetColor) $(el).val(hex);
+
+ if(!settings['display']['colors']['genes'][selected_genes[0].genomeID])
+ settings['display']['colors']['genes'][selected_genes[0].genomeID] = {};
+ selected_genes.forEach(gene => {
+ gene.fill = '#' + hex;
+ gene.dirty = true;
+ settings['display']['colors']['genes'][selected_genes[0].genomeID][gene.geneID] = '#' + hex;
+ });
+ canvas.renderAll();
+ }
+ }).keyup(function() {
+ $(this).colpickSetColor(this.value);
+ selected_genes.forEach(gene => {
+ gene.fill = this.value;
+ });
+ });
+
+ if(showSetLabel) {
+ $('#create_gene_set_label').on('change', function() {
+ createGeneSetLabel(selected_genes, $(this).val());
+ });
+ }
+}
+
+function createGeneSetLabel(selected_genes, title) {
+ // assume selected_genes are from the same genomeID
+ let genomeID = selected_genes[0].genomeID;
+ let geneIDs = selected_genes.map(gene => gene.geneID);
+
+ // if no set labels yet for this genome, initialize empty array
+ if(!settings['display']['labels']['set-labels'][genomeID]) {
+ let numGenes = Object.keys(settings['genomeData']['genomes'].find(genome => genome[0] == genomeID)[1].genes.gene_calls).length;
+ settings['display']['labels']['set-labels'][genomeID] = new Array(numGenes).fill(false);
+ settings['display']['labels']['gene-sets'][genomeID] = [];
+ } else {
+ // if a gene in this selection is already part of a set, do not create a new label
+ if(geneIDs.some(
+ geneID => settings['display']['labels']['set-labels'][genomeID][geneID]
+ )) return;
+ }
+
+ // save label to settings for redrawing
+ geneIDs.forEach(id => {
+ settings['display']['labels']['set-labels'][genomeID][id] = true
+ });
+ settings['display']['labels']['gene-sets'][genomeID].push([title, geneIDs]);
+
+ drawer.draw();
+}
+
+function applyLasso() {
+ $('#lasso-menu-body').empty().hide();
+
+ // TODO: apply any relevant changes here.
+ // Alternatively, we could apply changes immediately and remove this button.
+}
+
+
+
+function toggleSettingsPanel() {
+ $('#settings-panel').toggle();
+
+ if ($('#settings-panel').is(':visible')) {
+ $('#toggle-panel-settings').addClass('toggle-panel-settings-pos');
+ $('#toggle-panel-settings-inner').html('►');
+ } else {
+ $('#toggle-panel-settings').removeClass('toggle-panel-settings-pos');
+ $('#toggle-panel-settings-inner').html('◀');
+ }
+}
+
+function buildGenomesTable(genomes, order){
+ $("#tbody_genomes").empty() // clear table before redraw
+ genomes.map(genome => {
+ var height = '50';
+ var margin = '15';
+ let genomeLabel= genome[0];
+ var template = `
+
+ ${genomeLabel}
+ n/a
+ n/a
+ n/a
+
+
+ n/a
+ n/a
+
+ ;`
+
+ $('#tbody_genomes').append(template);
+ })
+
+ $("#tbody_genomes").sortable({helper: fixHelperModified, handle: '.drag-icon', items: "> tr"}).disableSelection();
+
+ $("#tbody_genomes").on("sortupdate", (event, ui) => {
+ changeGenomeOrder($("#tbody_genomes").sortable('toArray'))
+ })
+}
+
+function buildGroupLayersTable(layerLabel){
+ let color;
+ let show = settings['display']['layers'][layerLabel]
+
+ if(layerLabel == 'GC_Content') color = settings['display']['colors']['GC_Content'] // TODO: replace hardcoded values w state data
+ if(layerLabel == 'Coverage') color = settings['display']['colors']['coverage']
+
+ if(layerLabel === 'Ruler' || layerLabel === 'Genome'){
+ var template = `
+
+ ${layerLabel}
+ n/a
+
+ ;`
+ } else {
+ var template = `
+
+ ${layerLabel} ' +
+
+
+ ;`
+ }
+ $('#tbody_additionalDataLayers').append(template);
+ $("#tbody_additionalDataLayers").sortable({helper: fixHelperModified, handle: '.drag-icon', items: "> tr"}).disableSelection();
+
+ $(`#${layerLabel}-show`).prop('checked', show) // needs to trigger after the template layer is appended to the DOM
+
+ $("#tbody_additionalDataLayers").on("sortupdate", (event, ui) => {
+ changeGroupLayersOrder($("#tbody_additionalDataLayers").sortable('toArray'))
+ })
+
+ $(`#${layerLabel}_color`).colpick({
+ layout: 'hex',
+ submit: 0,
+ colorScheme: 'light',
+ onChange: function(hsb, hex, rgb, el, bySetColor) {
+ $(el).css('background-color', '#' + hex);
+ $(el).attr('color', '#' + hex);
+ if(layerLabel == 'Coverage' && settings['display']['colors']['coverage'] != `#${hex}`){
+ settings['display']['colors']['coverage'] = `#${hex}`
+ drawer.draw()
+ }
+ else if(layerLabel == 'GC_Content' && settings['display']['colors']['GC_Content'] != `#${hex}`){
+ settings['display']['colors']['GC_Content'] = `#${hex}`
+ drawer.draw()
+ } else {
+ $(el).val(hex);
+ }
+ }
+ }).keyup(function() {
+ $(this).colpickSetColor(this.value);
+ });
+}
+
+function toggleAdditionalDataLayer(e){
+ let layer = e.target.id.split('-')[0]
+
+ if(e.target.checked){
+ settings['display']['layers'][layer] = true
+ maxGroupSize += 1
+ } else {
+ settings['display']['layers'][layer] = false
+ maxGroupSize -= 1 // decrease group height if hiding the layer
+ }
+ drawer.draw()
+}
+
+/*
+ * capture user bookmark values and store obj in state
+ */
+function createBookmark(){
+ if(!$('#create_bookmark_input').val()){
+ alert('please provide a name for your bookmark :)')
+ return
+ }
+ try {
+ settings['display']['bookmarks'].push(
+ {
+ name : $('#create_bookmark_input').val(),
+ start : $('#brush_start').val(),
+ stop : $('#brush_end').val(),
+ description : $('#create_bookmark_description').val(),
+ }
+ )
+ toastr.success('bookmark successfully created :)')
+ $('#create_bookmark_input').val('')
+ $('#create_bookmark_description').val('')
+ $('#bookmarks-select').empty()
+ $('#bookmarks-select').prepend('Bookmarks ')
+
+ settings['display']['bookmarks'].map(bookmark => {
+ $('#bookmarks-select').append((new Option(bookmark['name'], [bookmark["start"], bookmark['stop']])))
+ })
+ } catch (error) {
+ toastr.error(`anvi'o was unable to save your bookmark because of an error ${error} :/`)
+ // throw to error landing page?
+ }
+}
+/*
+ * update sequence position, bookmark description upon user select from dropdown
+ */
+function respondToBookmarkSelect(){
+ $('#bookmarks-select').change(function(e){
+ let [start, stop] = [$(this).val().split(',')[0], $(this).val().split(',')[1] ]
+ if(!stop)return // edge case for empty 'Bookmarks' placeholder select value
+ $('#brush_start').val(start);
+ $('#brush_end').val(stop);
+ try {
+ brush.extent([start, stop]);
+ brush(d3.select(".brush").transition());
+ brush.event(d3.select(".brush").transition());
+ let selectedBookmark = settings['display']['bookmarks'].find(bookmark => bookmark.start == start && bookmark.stop == stop)
+ $('#bookmark-description').text(selectedBookmark['description'])
+ toastr.success("Bookmark successfully loaded")
+ } catch (error) {
+ toastr.warn(`Unable to load bookmark because of an error ${error}`)
+ }
+ })
+}
+
+/*
+ * respond to ui, redraw with updated group layer order
+ */
+function changeGroupLayersOrder(updatedOrder){
+ settings['group-layer-order'] = updatedOrder
+ drawer.draw()
+}
+
+/*
+ * respond to ui, redraw with updated genome group order
+ */
+function changeGenomeOrder(updatedOrder){
+ let newGenomeOrder = []
+ updatedOrder.map(label => {
+ newGenomeOrder.push(settings['genomeData']['genomes'].find(genome => genome[0] == label))
+ })
+ settings['genomeData']['genomes'] = newGenomeOrder
+ drawer.draw()
+}
+
+
+/*
+ * Generates functional annotation color table for a given color palette.
+ *
+ * @param fn_colors : dict matching each category to a hex color code to override defaults
+ * @param fn_type : string indicating function category type
+ * @param highlight_genes : array of format [{genomeID: 'g01', geneID: 3, color: '#FF0000'}, ...] to override other coloring for specific genes
+ * @param filter_to_window : if true, filters categories to only those shown in the current render window
+ * @param sort_by_count : if true, sort annotations by # occurrences, otherwise sort alphabetically
+ * @param thresh_count : int indicating min # occurences required for a given category to be included in the table
+ */
+function generateColorTable(fn_colors, fn_type, highlight_genes=null, filter_to_window=filter_gene_colors_to_window, sort_by_count=order_gene_colors_by_count, thresh_count = thresh_count_gene_colors) {
+ let db;
+ if(fn_type == 'Source') {
+ db = default_source_colors;
+ } else {
+ counts = [];
+
+ // Traverse categories
+ for(genome of settings['genomeData']['genomes']) {
+ let gene_calls = genome[1].genes.gene_calls;
+ let gene_funs = genome[1].genes.functions;
+
+ for(let i = 0; i < Object.keys(gene_calls)[Object.keys(gene_calls).length-1]; i++) {
+ if(filter_to_window && (gene_calls[i].start < renderWindow[0] || gene_calls[i].stop > renderWindow[1])) continue;
+ let cag = getCagForType(gene_funs[i], fn_type);
+ counts.push(cag ? cag : "None");
+ }
+ }
+
+ // Get counts for each category
+ counts = counts.reduce((counts, val) => {
+ counts[val] = counts[val] ? counts[val]+1 : 1;
+ return counts;
+ }, {});
+
+ // Filter by count
+ let count_removed = 0;
+ counts = Object.fromEntries(
+ Object.entries(counts).filter(([cag,count]) => {
+ if(count < thresh_count) count_removed += count;
+ return count >= thresh_count || cag == "None";
+ })
+ );
+
+ // Save pre-sort order
+ let order = {};
+ for(let i = 0; i < Object.keys(counts).length; i++) {
+ order[Object.keys(counts)[i]] = i;
+ }
+
+ // Sort categories
+ counts = Object.fromEntries(
+ Object.entries(counts).sort(function(first, second) {
+ return sort_by_count ? second[1] - first[1] : first[0].localeCompare(second[0]);
+ })
+ );
+ if(count_removed > 0) counts["Other"] = count_removed;
+
+ // Create custom color dict from categories
+ db = getCustomColorDict(fn_type, cags=Object.keys(counts), order=order);
+ if(Object.keys(db).includes("Other") && !db["Other"]) db["Other"] = "#FFFFFF"; // bug fix
+ }
+
+ // Override default values with any values supplied to fn_colors
+ if(fn_colors) {
+ Object.keys(db).forEach(cag => { if(Object.keys(fn_colors).includes(cag)) db[cag] = fn_colors[cag] });
+ }
+
+ $('#tbody_function_colors').empty();
+ Object.keys(db).forEach(category => {
+ appendColorRow(fn_type == 'Source' ? category : category + " (" + counts[category] + ")", category, db[category]);
+ $('#picker_' + getCleanCagCode(category)).colpick({
+ layout: 'hex',
+ submit: 0,
+ colorScheme: 'light',
+ onChange: function(hsb, hex, rgb, el, bySetColor) {
+ $(el).css('background-color', '#' + hex);
+ $(el).attr('color', '#' + hex);
+ // TODO: save new color once state is implemented
+ //state[$('#gene_color_order').val().toLowerCase() + '-colors'][el.id.substring(7)] = '#' + hex;
+ if (!bySetColor) $(el).val(hex);
+ if(!settings.display.colors.genes.annotations[fn_type]) settings.display.colors.genes.annotations[fn_type] = {};
+ settings.display.colors.genes.annotations[fn_type][category] = '#' + hex;
+ }
+ }).keyup(function() {
+ $(this).colpickSetColor(this.value);
+ });
+ });
+
+ /*$('.colorpicker').colpick({
+ layout: 'hex',
+ submit: 0,
+ colorScheme: 'light',
+ onChange: function(hsb, hex, rgb, el, bySetColor) {
+ $(el).css('background-color', '#' + hex);
+ $(el).attr('color', '#' + hex);
+ // TODO: save new color once state is implemented
+ //state[$('#gene_color_order').val().toLowerCase() + '-colors'][el.id.substring(7)] = '#' + hex;
+ if (!bySetColor) $(el).val(hex);
+ }
+ }).keyup(function() {
+ $(this).colpickSetColor(this.value);
+ settings['display']['colors']['genes']['annotations'][fn_type][category] = '#' + hex;
+ });*/
+
+ if(highlight_genes) {
+ let genomes = Object.entries(settings['genomeData']['genomes']).map(g => g[1][0]);
+ for(entry of highlight_genes) {
+ let genomeID = entry['genomeID'];
+ let geneID = entry['geneID'];
+ let color = entry['color'];
+
+ if(!genomes.includes(genomeID)) continue;
+
+ let ind = settings['genomeData']['genomes'].findIndex(g => g[0] == genomeID);
+ let genes = Object.keys(settings['genomeData']['genomes'][ind][1].genes.gene_calls);
+ if(!(geneID in genes)) continue;
+
+ let label = 'Genome: ' + genomeID + ', Gene: ' + geneID;
+ appendColorRow(label, genomeID + '-' + geneID, color, prepend=true);
+ }
+ $('colorpicker').colpick({
+ layout: 'hex',
+ submit: 0,
+ colorScheme: 'light',
+ onChange: function(hsb, hex, rgb, el, bySetColor) {
+ $(el).css('background-color', '#' + hex);
+ $(el).attr('color', '#' + hex);
+ //state['highlight-genes'][el.id.substring(7)] = '#' + hex;
+ if (!bySetColor) $(el).val(hex);
+ }
+ }).keyup(function() {
+ $(this).colpickSetColor(this.value);
+ });
+ }
+}
+
+/*
+ * [TO BE ADDED to genomeview/UI.js OR 'regular' utils.js]
+ * Resets function color table to the default set
+ *
+ * @param fn_colors: If set, resets state to this dictionary instead of the defaults.
+ */
+function resetFunctionColors(fn_colors=null) {
+ // TODO: this should reset color dictionaries in state and then redraw table, once state is implemented
+ if($('#gene_color_order') == null) return;
+ delete settings['display']['colors']['genes']['annotations'][color_db];
+ generateColorTable(fn_colors, color_db);
+}
+
+/*
+ * Responds to 'Apply' button in Settings panel under batch coloring
+ */
+ function batchColor() {
+ var rule = $('[name=batch_rule]:checked').val()
+ var color = settings['display']['colors']['Batch'];
+ var randomize_color = $('#batch_randomcolor').is(':checked');
+
+ let fn_type = $('#gene_color_order').val();
+ let dict = getCustomColorDict(fn_type);
+ if(counts && Object.keys(counts).includes("Other")) dict["Other"] = $("#picker_Other").attr('color') ? $("#picker_Other").attr('color') : "#FFFFFF";
+
+ Object.keys(dict).forEach(category => {
+ if(randomize_color) {
+ // color = randomColor();
+ // TODO: instead of using default color dict for random colors, dynamically create random color here to avoid similarity to chosen group color
+ }
+ let code = getCleanCagCode(category);
+ if(rule == 'all') {
+ $("#picker_" + code).colpickSetColor(color);
+ } else if(rule == 'name') {
+ if(category.toLowerCase().indexOf($('#name_rule').val().toLowerCase()) > -1) {
+ $("#picker_" + code).colpickSetColor(color);
+ }
+ } else if(rule == 'count') {
+ let count = counts[category];
+ if (eval(count + unescape($('#count_rule').val()) + " " + parseFloat($('#count_rule_value').val()))) {
+ $("#picker_" + code).colpickSetColor(color);
+ }
+ }
+ });
+
+ drawer.draw();
+ }
+
+function buildGeneLabelsSelect(){
+ let sourcesObj = {}
+ settings['genomeData']['genomes'].map(genome => {
+ Object.values(genome[1]['genes']['functions']).map(func => {
+ Object.keys(func).forEach(source => {
+ if(sourcesObj[source]){
+ return
+ } else {
+ sourcesObj[source] = true
+ }
+ })
+ })
+ })
+ Object.keys(sourcesObj).forEach(source => {
+ $("#gene_label_source").append(new Option(source, source));
+ // we can also build the dropdown UI element for source-selection in functional querying
+ $("#function_search_category").append(new Option(source, source))
+ })
+ // while we're here, we add 'metadata' to the function_search_category dropdown select
+ // TODO refactor naming convention to sequence_search_category, bc we're not just querying functions!
+ $('#function_search_category').append(new Option('metadata', 'metadata'))
+}
+
+function setGeneVisibilityRange(targetGene, targetGenome){
+ let genomeOfInterest = settings['genomeData']['genomes'].filter(genome => genome[0] == targetGenome)
+ let maxGeneID = Object.keys(genomeOfInterest[0][1]['genes']['dna']).length
+ settings['display']['hidden'][targetGenome] = {}
+ let startingPoint = parseInt(targetGene)
+ let lowRangeStart = startingPoint - parseInt($('#hide-range-low').val())
+ let highRangeStart = startingPoint + parseInt($('#hide-range-high').val())
+
+ lowRangeStart < 0 ? lowRangeStart = 0 : null
+ highRangeStart > parseInt(maxGeneID) ? highRangeStart = maxGeneID : null
+
+ for(let i = lowRangeStart; i >= 0; i--){
+ settings['display']['hidden'][targetGenome][i] = true
+ }
+ for(let i = highRangeStart; i <= parseInt(maxGeneID); i++){
+ settings['display']['hidden'][targetGenome][i] = true
+ }
+ $('#deepdive-modal-body').modal('toggle');
+ drawer.draw()
+}
+
+function showAllHiddenGenes(){
+ settings['display']['hidden'] = {}
+ drawer.draw()
+}
\ No newline at end of file
diff --git a/anvio/data/interactive/js/genomeview/utils.js b/anvio/data/interactive/js/genomeview/utils.js
new file mode 100644
index 0000000000..7becfe1e5f
--- /dev/null
+++ b/anvio/data/interactive/js/genomeview/utils.js
@@ -0,0 +1,370 @@
+/**
+ * Javascript library for anvi'o genome view
+ *
+ * Authors: Isaac Fink
+ * Matthew Klein
+ * A. Murat Eren
+ *
+ * Copyright 2021, The anvi'o project (http://anvio.org)
+ *
+ * Anvi'o is a free software. You can redistribute this program
+ * and/or modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation, either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with anvi'o. If not, see .
+ *
+ * @license GPL-3.0+
+ */
+
+/**
+ * File Overview : This file contains utility functions used throughout genomeview. As a general rule,
+ * functions defined here explicitly return some value or mutate an existing global variable. Functions
+ * defined here should not interact directly with elements of UI, state, or canvas objects.
+ */
+
+
+/*
+ * return height value for main canvas element
+ */
+function calculateMainCanvasHeight(){
+ let additionalSpacing = 100 // arbitrary additional spacing for cosmetics
+ let mainCanvasHeight = spacing * settings['genomeData']['genomes'].length + additionalSpacing + (spacing * maxGroupSize * groupLayerPadding)
+ return mainCanvasHeight
+}
+
+/*
+ * Save NT length of the largest genome in `genomeMax`.
+ */
+function calculateMaxGenomeLength(){
+ for(genome of genomeData.genomes) {
+ genome = genome[1].genes.gene_calls;
+ let genomeEnd = genome[Object.keys(genome).length-1].stop;
+ if(genomeEnd > genomeMax) genomeMax = genomeEnd;
+ }
+}
+
+/*
+ * @returns NT position of the middle of each gene in a given genome with a specified gene cluster
+ */
+function getGenePosForGenome(genomeID, gc) {
+ var targetGenes = getGenesOfGC(genomeID, gc);
+ if(targetGenes == null) return null;
+
+ let genome = settings['genomeData']['genomes'].find(g => g[0] == genomeID);
+ let mids = [];
+ for(geneID of targetGenes) {
+ let gene = genome[1].genes.gene_calls[geneID];
+ let geneMid = gene.start + (gene.stop - gene.start) / 2;
+ mids.push(geneMid);
+ }
+ return mids;
+}
+
+/*
+ * @returns array of geneIDs in a given genome with a specified gene cluster
+ */
+function getGenesOfGC(genomeID, gc) {
+ var targetGenes = settings['genomeData']['gene_associations']["anvio-pangenome"]["gene-cluster-name-to-genomes-and-genes"][gc][genomeID];
+ return targetGenes.length > 0 ? targetGenes : null;
+}
+
+/*
+ * Show/hide gene labels to show the max amount possible s.t. none overlap.
+ */
+function checkGeneLabels() {
+ var labels = canvas.getObjects().filter(obj => obj.id == 'geneLabel');
+ for(var i = 0; i < labels.length-1; i++) {
+ if(this.settings['display']['arrow-style'] == 3) {
+ // hide labels that don't fit inside pentagon arrows
+ if(labels[i].width/2 > canvas.getObjects().filter(obj => obj.id == 'arrow')[i].width) {
+ labels[i].visible = false;
+ continue;
+ }
+ labels[i].visible = true;
+ }
+ var p = i+1;
+ while(p < labels.length && labels[i].intersectsWithObject(labels[p])) {
+ labels[p].visible = false;
+ p++;
+ }
+ if(p == labels.length) return;
+ labels[p].visible = true;
+ i = p - 1;
+ }
+}
+
+ /*
+ * @returns [start, stop] nt range for the current viewport and scaleFactor
+ */
+ function getNTRangeForVPT() {
+ let vpt = canvas.viewportTransform;
+ let window_left = Math.floor((-1*vpt[4]-xDisplacement)/scaleFactor);
+ let window_right = Math.floor(window_left + canvas.getWidth()/scaleFactor);
+ // if window is out of bounds, shift to be in bounds
+ if(window_left < 0) {
+ window_right -= window_left;
+ window_left = 0;
+ }
+ if(window_right > genomeMax) {
+ window_left -= (window_right - genomeMax);
+ window_right = genomeMax;
+ }
+ return [window_left, window_right];
+}
+
+/*
+ * @returns [start, stop] proportional (0-1) range, used with scale for non-aligned genomes
+ */
+function getFracForVPT() {
+ let resolution = 4; // number of decimals to show
+ let [x1, x2] = calcXBounds();
+ let window_left = Math.round(10**resolution * (-1*canvas.viewportTransform[4] - x1) / (x2 - x1)) / 10**resolution;
+ let window_right = Math.round(10**resolution * (window_left + (canvas.getWidth()) / (x2 - x1))) / 10**resolution;
+ // if window is out of bounds, shift to be in bounds
+ if(window_left < 0) {
+ window_right -= window_left;
+ window_left = 0;
+ }
+ if(window_right > 1) {
+ window_left -= (window_right - 1);
+ window_right = 1;
+ }
+ return [window_left, window_right];
+}
+
+/*
+ * @returns range of renderWindow x-positions for a given proportional range
+ */
+function getRenderXRangeForFrac() {
+ if(!percentScale) return null;
+ let [l,r] = calcXBounds();
+ let [x1, x2] = renderWindow.map(x => l+x*(r-l));
+ return [x1, x2];
+}
+
+function getRenderNTRange(genomeID) {
+ if(!percentScale) return renderWindow;
+ let [l,r] = calcXBounds();
+ let [start, end] = getRenderXRangeForFrac().map(x => (x-xDisps[genomeID])/scaleFactor);
+ return [clamp(start,0,genomeMax), clamp(end,0,genomeMax)];
+}
+
+/*
+ * @returns array [min, max] where
+ * min = x-start of the leftmost genome, max = x-end of the rightmost genome
+ */
+function calcXBounds() {
+ let min = 9*(10**9), max = -9*(10**9);
+ for(let g in xDisps) {
+ if(xDisps[g] > max) max = xDisps[g];
+ if(xDisps[g] < min) min = xDisps[g];
+ }
+ return [min, max + scaleFactor*genomeMax];
+}
+
+/*
+ * @returns array of functional annotation types from table in `genomeData`
+ */
+function getFunctionalAnnotations() {
+ return Object.keys(genomeData.genomes[0][1].genes.functions[0]);
+}
+
+/*
+ * @returns arbitrary category:color dict given a list of categories
+ */
+function getCustomColorDict(fn_type, cags=null, order=null) {
+ if(!Object.keys(genomeData.genomes[0][1].genes.functions[0]).includes(fn_type)) return null;
+
+ if(!cags) {
+ cags = [];
+ genomeData.genomes.forEach(genome => {
+ Object.values(genome[1].genes.functions).forEach(fn => {
+ let cag = getCagForType(fn, fn_type);
+ if(cag && !cags.includes(cag)) cags.push(cag);
+ if(!cag && !cags.includes("None")) cags.push("None");
+ });
+ });
+ }
+
+ // move "Other" and "None" to end of list
+ if(cags.includes("Other")) cags.push(cags.splice(cags.indexOf("Other"), 1)[0]);
+ if(cags.includes("None")) cags.push(cags.splice(cags.indexOf("None"), 1)[0]);
+
+ let dict = custom_cag_colors.reduce((out, field, index) => {
+ out[cags[index]] = field;
+ return out;
+ }, {});
+
+ // sort using order
+ if(order) {
+ let colors = Object.values(dict);
+ Object.keys(dict).forEach(cag => { dict[cag] = colors[order[cag]] });
+ }
+
+ if(dict["Other"]) dict["Other"] = "#FFFFFF";
+ if(dict["None"]) dict["None"] = "#808080";
+ delete dict["undefined"];
+ return dict;
+}
+
+function orderColorTable(order) {
+ order_gene_colors_by_count = order == 'count';
+ generateColorTable(null, $("#gene_color_order").val());
+}
+
+function filterColorTable(thresh) {
+ if(isNaN(thresh)) {
+ alert("Error: filtering threshold must be numeric");
+ return;
+ } else if(thresh < 1) {
+ alert("Error: filtering threshold must be an integer >= 1");
+ return;
+ }
+ thresh_count_gene_colors = thresh;
+ generateColorTable(null, $("#gene_color_order").val());
+ drawer.draw();
+}
+
+function showSaveStateWindow(){
+ $.ajax({
+ type: 'GET',
+ cache: false,
+ url: '/state/all',
+ success: function(state_list) {
+ $('#saveState_list').empty();
+
+ for (let state_name in state_list) {
+ var _select = "";
+ if (state_name == current_state_name)
+ {
+ _select = ' selected="selected"';
+ }
+ $('#saveState_list').append('' + state_name + ' ');
+ }
+
+ $('#modSaveState').modal('show');
+ if ($('#saveState_list').val() === null) {
+ $('#saveState_name').val('default');
+ } else {
+ $('#saveState_list').trigger('change');
+ }
+ },
+ error: function(error){
+ console.log('got an error', error)
+ }
+ });
+}
+
+function showLoadStateWindow(){
+ $.ajax({
+ type: 'GET',
+ cache: false,
+ url: '/state/all',
+ success: function(state_list) {
+ $('#loadState_list').empty();
+
+ for (let state_name in state_list) {
+ $('#loadState_list').append('' + state_name + ' ');
+ }
+
+ $('#modLoadState').modal('show');
+ }
+ });
+}
+
+function saveState()
+{
+ var name = $('#saveState_name').val();
+
+ if (name.length==0) {
+ $('#saveState_name').focus();
+ return;
+ }
+
+ var state_exists = false;
+
+ $.ajax({
+ type: 'GET',
+ cache: false,
+ async: false,
+ url: '/state/all',
+ success: function(state_list) {
+ for (let state_name in state_list) {
+ if (state_name == name)
+ {
+ state_exists = true;
+ }
+ }
+
+ }
+ });
+
+ if (state_exists && !confirm('"' + name + '" already exist, do you want to overwrite it?')) {
+ return;
+ }
+
+ $.ajax({
+ type: 'POST',
+ cache: false,
+ url: '/state/save/' + name,
+ data: {
+ 'content': JSON.stringify(serializeSettings())
+ },
+ success: function(response) {
+ if (typeof response != 'object') {
+ response = JSON.parse(response);
+ }
+
+ if (response['status_code']==0)
+ {
+ toastr.error("Failed, Interface running in read only mode.");
+ }
+ else if (response['status_code']==1)
+ {
+ // successfull
+ $('#modSaveState').modal('hide');
+
+ current_state_name = name;
+ toastr.success("State '" + current_state_name + "' successfully saved.");
+ }
+ }
+ });
+}
+
+function toggleRightPanel(name) {
+ ['#mouseover_panel', '#settings-panel', '#query-panel'].forEach(function(right_panel) {
+ if (right_panel == name)
+ return;
+
+ $(right_panel).hide();
+ });
+ console.log(name);
+ $(name).toggle();
+
+ if ($('#mouseover_panel').is(':visible')) {
+ $('#toggle-panel-mouseover').addClass('toggle-panel-mouseover-pos');
+ $('#toggle-panel-mouseover-inner').html('►');
+ } else {
+ $('#toggle-panel-mouseover').removeClass('toggle-panel-mouseover-pos');
+ $('#toggle-panel-mouseover-inner').html('◀');
+ }
+
+ if ($('#settings-panel').is(':visible')) {
+ $('#toggle-panel-settings').addClass('toggle-panel-settings-pos');
+ $('#toggle-panel-settings-inner').html('►');
+ } else {
+ $('#toggle-panel-settings').removeClass('toggle-panel-settings-pos');
+ $('#toggle-panel-settings').html('◀');
+ }
+
+ if ($('#query-panel').is(':visible')) {
+ $('#toggle-panel-query').addClass('toggle-panel-query-pos');
+ $('#toggle-panel-query-inner').html('►');
+ } else {
+ $('#toggle-panel-query').removeClass('toggle-panel-query-pos');
+ $('#toggle-panel-query-inner').html('◀');
+ }
+}
+
diff --git a/anvio/data/interactive/js/help-messages.js b/anvio/data/interactive/js/help-messages.js
index 47222c471c..e1b8b7d493 100644
--- a/anvio/data/interactive/js/help-messages.js
+++ b/anvio/data/interactive/js/help-messages.js
@@ -1,3 +1,22 @@
+/**
+ * Constants for tooltips
+ *
+ * Authors: A. Murat Eren
+ * Ozcan Esen
+ *
+ * Copyright 2015-2021, The anvi'o project (http://anvio.org)
+ *
+ * Anvi'o is a free software. You can redistribute this program
+ * and/or modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation, either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with anvi'o. If not, see .
+ *
+ * @license GPL-3.0+
+ */
+
var help_contents = {
'load-state-button': 'Load a previously stored visual state from the profile database',
'save-state-button': 'Save a snapshot of all user settings for layers into the profile database',
diff --git a/anvio/data/interactive/js/inspectionutils.js b/anvio/data/interactive/js/inspectionutils.js
index 8e091173d0..1932d5fcfb 100644
--- a/anvio/data/interactive/js/inspectionutils.js
+++ b/anvio/data/interactive/js/inspectionutils.js
@@ -1,11 +1,11 @@
/**
* Helper funcitons for amvi'o inspection pages
*
- * Author: A. Murat Eren
- * Credits: Özcan Esen, Gökmen Göksel, Tobias Paczian.
- * Copyright 2015, The anvio Project
+ * Authors: A. Murat Eren
+ * Ozcan Esen
+ * Isaac Fink
*
- * This file is part of anvi'o ().
+ * Copyright 2015-2021, The anvi'o project (http://anvio.org)
*
* Anvi'o is a free software. You can redistribute this program
* and/or modify it under the terms of the GNU General Public
@@ -290,28 +290,23 @@ function drawArrows(_start, _stop, colortype, gene_offset_y, color_genes=null) {
var y = 10 + (gene.level * 20);
- var category = "none";
- if(colortype == "COG") {
- if(gene.functions !== null && gene.functions.hasOwnProperty("COG_CATEGORY") && gene.functions.COG_CATEGORY != null) {
- category = gene.functions["COG_CATEGORY"][0][0];
- }
- if(category == null || category == "X") category = "none";
- } else if(colortype == "KEGG") {
- if(gene.functions !== null && gene.functions.hasOwnProperty("KEGG_Class") && gene.functions.KEGG_Class != null) {
- category = getCategoryForKEGGClass(gene.functions["KEGG_Class"][1]);
- }
- if(category == null) category = "none";
- } else if(colortype == "Source") {
- if (gene.source.startsWith('Ribosomal_RNA')) {
- category = 'rRNA';
- } else if (gene.source == 'Transfer_RNAs') {
- category = 'tRNA';
- } else if (gene.functions !== null) {
- category = 'Function';
- } else {
- category = "None";
+
+ let category = getCagForType(gene.functions, colortype);
+ category = getCleanCagCode(category);
+
+ if(!category) {
+ category = "None";
+ if(colortype == "Source") {
+ if (gene.source.startsWith('Ribosomal_RNA')) {
+ category = 'rRNA';
+ } else if (gene.source == 'Transfer_RNAs') {
+ category = 'tRNA';
+ } else if (gene.functions !== null) {
+ category = 'Function';
+ }
}
}
+
if(color_genes != null && !isEmpty(color_genes) && color_genes.includes("" + gene.gene_callers_id)) {
category = gene.gene_callers_id;
}
@@ -330,10 +325,11 @@ function drawArrows(_start, _stop, colortype, gene_offset_y, color_genes=null) {
}
// M10 15 l20 0
+ let color = $('#picker_' + category).length > 0 ? $('#picker_' + category).attr('color') : $('#picker_Other').attr('color');
path = paths.append('svg:path')
.attr('id', 'gene_' + gene.gene_callers_id)
.attr('d', 'M' + start +' '+ y +' l'+ stop +' 0')
- .attr('stroke', category == "none" ? "gray" : $('#picker_' + category).attr('color'))
+ .attr('stroke', color)
.attr('stroke-width', 6)
.attr("style", "cursor:pointer;")
.attr('marker-end', function() {
@@ -388,6 +384,68 @@ function getGeneEndpts(_start, _stop) {
return ret;
}
+/*
+ * @returns arbitrary category:color dict given a list of categories
+ */
+function getCustomColorDict(fn_type, cags=null, order=null) {
+ if(fn_type == "Source") return default_source_colors;
+
+ if(!cags) {
+ cags = Object.values(geneParser["data"]).map(gene => gene.functions ? getCagForType(gene.functions, fn_type) : null)
+ .filter(o => { if(!o) o = "None"; return o != null });
+ cags = cags.filter((item, i) => { return cags.indexOf(item) == i }); // remove duplicates
+ }
+
+ // move "Other" and "None" to end of list
+ if(cags.includes("Other")) cags.push(cags.splice(cags.indexOf("Other"), 1)[0]);
+ if(cags.includes("None")) cags.push(cags.splice(cags.indexOf("None"), 1)[0]);
+
+ let out = custom_cag_colors.reduce((out, field, index) => {
+ out[cags[index]] = field;
+ return out;
+ }, {});
+
+ // sort using order
+ if(order) {
+ let colors = Object.values(out);
+ Object.keys(out).forEach(cag => { out[cag] = colors[order[cag]] });
+ }
+
+ if(cags.includes("Other")) out["Other"] = "#FFFFFF";
+ if(cags.includes("None")) out["None"] = "#808080";
+ delete out["undefined"];
+ return out;
+}
+
+/*
+ * @returns array of functional annotation types from genes
+ */
+function getFunctionalAnnotations() {
+ for(gene of geneParser["data"]) {
+ if(!gene.functions) continue;
+ return Object.keys(gene.functions);
+ }
+ return [];
+}
+
+function orderColorTable(order) {
+ order_gene_colors_by_count = order == 'count';
+ generateFunctionColorTable(null, $("#gene_color_order").val());
+}
+
+function filterColorTable(thresh) {
+ if(isNaN(thresh)) {
+ alert("Error: filtering threshold must be numeric");
+ return;
+ } else if(thresh < 1) {
+ alert("Error: filtering threshold must be an integer >= 1");
+ return;
+ }
+ thresh_count_gene_colors = thresh;
+ generateFunctionColorTable(null, $("#gene_color_order").val());
+ redrawArrows();
+}
+
var base_colors = ['#CCB48F', '#727EA3', '#65567A', '#CCC68F', '#648F7D', '#CC9B8F', '#A37297', '#708059'];
function get_comp_nt_color(nts){
@@ -405,22 +463,6 @@ function get_comp_nt_color(nts){
return "black";
}
-function getCategoryForKEGGClass(class_str) {
- if(class_str == null) return null;
-
- var category_name = getClassFromKEGGAnnotation(class_str);
- return getKeyByValue(KEGG_categories, category_name);
-}
-
-function getClassFromKEGGAnnotation(class_str) {
- return class_str.substring(17, class_str.indexOf(';', 17));
-}
-
-// https://stackoverflow.com/questions/9907419/how-to-get-a-key-in-a-javascript-object-by-its-value/36705765
-function getKeyByValue(object, value) {
- return Object.keys(object).find(key => object[key] === value);
-}
-
// https://stackoverflow.com/questions/16947100/max-min-of-large-array-in-js
function GetMaxMin(input_array) {
var max = Number.MIN_VALUE, min = Number.MAX_VALUE;
diff --git a/anvio/data/interactive/js/main.js b/anvio/data/interactive/js/main.js
index e2bbadce28..f14da0c3de 100644
--- a/anvio/data/interactive/js/main.js
+++ b/anvio/data/interactive/js/main.js
@@ -1,11 +1,11 @@
/**
* Javascript library for anvi'o interactive interface
*
- * Author: Özcan Esen
- * Credits: A. Murat Eren, Doğan Can Kilment
- * Copyright 2015, The anvio Project
+ * Authors: A. Murat Eren
+ * Ozcan Esen
+ * Isaac Fink
*
- * This file is part of anvi'o ().
+ * Copyright 2015-2021, The anvi'o project (http://anvio.org)
*
* Anvi'o is a free software. You can redistribute this program
* and/or modify it under the terms of the GNU General Public
diff --git a/anvio/data/interactive/js/migrations.js b/anvio/data/interactive/js/migrations.js
index 5cf3e48580..cb8404b428 100644
--- a/anvio/data/interactive/js/migrations.js
+++ b/anvio/data/interactive/js/migrations.js
@@ -1,3 +1,20 @@
+/**
+ * Javascript library for state migrations
+ *
+ * Authors: Ozcan Esen
+ *
+ * Copyright 2019-2021, The anvi'o project (http://anvio.org)
+ *
+ * Anvi'o is a free software. You can redistribute this program
+ * and/or modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation, either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with anvi'o. If not, see .
+ *
+ * @license GPL-3.0+
+ */
function migrate_state(state) {
let current_version = state['version'];
@@ -37,7 +54,7 @@ function migrate_state(state) {
for (let layer_name in state['stack_bar_colors']) {
let bar_names = layer_name.split('!')[1].split(';');
new_stack_bar_colors[layer_name] = {};
-
+
for (let j=0; j < bar_names.length; j++) {
new_stack_bar_colors[layer_name][bar_names[j]] = state['stack_bar_colors'][layer_name][j];
}
@@ -52,7 +69,7 @@ function migrate_state(state) {
for (let layer_name in state['samples-stack-bar-colors'][group]) {
let bar_names = layer_name.split('!')[1].split(';');
new_samples_stack_bar_colors[group][layer_name] = {};
-
+
for (let j=0; j < bar_names.length; j++) {
new_samples_stack_bar_colors[group][layer_name][bar_names[j]] = state['samples-stack-bar-colors'][group][layer_name][j];
}
@@ -70,6 +87,6 @@ function migrate_state(state) {
toastr.error(`Anvi'o failed to upgrade the state. State will be ignored.`);
return {}
}
-
+
return state;
}
diff --git a/anvio/data/interactive/js/mouse-events.js b/anvio/data/interactive/js/mouse-events.js
index bc5a719e36..8a3e1b114f 100644
--- a/anvio/data/interactive/js/mouse-events.js
+++ b/anvio/data/interactive/js/mouse-events.js
@@ -1,10 +1,11 @@
/**
* Javascript user input handler functions for anvi'o interactive interface
*
- * Author: Özcan Esen
- * Copyright 2015, The anvio Project
+ * Authors: Ozcan Esen
+ * Matthew Klein
+ * A. Murat Eren
*
- * This file is part of anvi'o ().
+ * Copyright 2015-2021, The anvi'o project (http://anvio.org)
*
* Anvi'o is a free software. You can redistribute this program
* and/or modify it under the terms of the GNU General Public
diff --git a/anvio/data/interactive/js/multiple.js b/anvio/data/interactive/js/multiple.js
index 4b74ef4ba6..6945722031 100644
--- a/anvio/data/interactive/js/multiple.js
+++ b/anvio/data/interactive/js/multiple.js
@@ -1,4 +1,21 @@
-// Edit Attributes For Multiple Layers
+/**
+ * Edit Attributes For Multiple Layers
+ *
+ * Authors: Ozcan Esen
+ * Dogan Can Kilment
+ *
+ * Copyright 2015-2021, The anvi'o project (http://anvio.org)
+ *
+ * Anvi'o is a free software. You can redistribute this program
+ * and/or modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation, either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with anvi'o. If not, see .
+ *
+ * @license GPL-3.0+
+ */
$(document).ready(function() {
$('.select_layer').on('change', function() {
@@ -85,7 +102,7 @@ $(document).ready(function() {
var row = $(this).parent().parent();
var combo = $(row).find(target_selector);
- if (combo.find('option[value="' + new_val + '"]').length > 0)
+ if (combo.find('option[value="' + new_val + '"]').length > 0)
{
combo.val(new_val).trigger('change');
return;
diff --git a/anvio/data/interactive/js/news.js b/anvio/data/interactive/js/news.js
index 883d200eb0..2ae427122f 100644
--- a/anvio/data/interactive/js/news.js
+++ b/anvio/data/interactive/js/news.js
@@ -1,3 +1,23 @@
+/**
+ * Javascript library to display anvi'o news.
+ *
+ * Authors: Ozcan Esen
+ * Isaac Fink
+ * A. Murat Eren
+ *
+ * Copyright 2015-2021, The anvi'o project (http://anvio.org)
+ *
+ * Anvi'o is a free software. You can redistribute this program
+ * and/or modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation, either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with anvi'o. If not, see .
+ *
+ * @license GPL-3.0+
+ */
+
$(document).ready(function() {
checkNews();
});
diff --git a/anvio/data/interactive/js/sample.js b/anvio/data/interactive/js/sample.js
index 1a4354b439..632c920469 100644
--- a/anvio/data/interactive/js/sample.js
+++ b/anvio/data/interactive/js/sample.js
@@ -1,3 +1,23 @@
+/**
+ * Javascript library to visualize additional layer info
+ * (previously known as samples db)
+ *
+ * Authors: Ozcan Esen
+ * A. Murat Eren
+ *
+ * Copyright 2015-2021, The anvi'o project (http://anvio.org)
+ *
+ * Anvi'o is a free software. You can redistribute this program
+ * and/or modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation, either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with anvi'o. If not, see .
+ *
+ * @license GPL-3.0+
+ */
+
var samples_categorical_colors = {};
var samples_categorical_stats = {};
var samples_stack_bar_colors = {};
@@ -91,7 +111,7 @@ $(document).ready(function() {
$('#group_select_all').click(function() {
let target_group = $('#group_list_for_select_all').val();
-
+
$('#tbody_samples tr').each((index, tr) => {
let group = $(tr).attr('samples-group-name');
@@ -103,7 +123,7 @@ $(document).ready(function() {
$('#group_unselect_all').click(function() {
let target_group = $('#group_list_for_select_all').val();
-
+
$('#tbody_samples tr').each((index, tr) => {
let group = $(tr).attr('samples-group-name');
@@ -224,7 +244,7 @@ function update_samples_layer_min_max(select) {
if (max === null || Math.sqrt(parseFloat(samples_information_dict[group][lname][sample_name])) > max) {
max = Math.sqrt(parseFloat(samples_information_dict[group][lname][sample_name]));
}
- }
+ }
else if (norm == 'log') {
if (max === null || log10(parseFloat(samples_information_dict[group][lname][sample_name]) + 1) > max) {
max = log10(parseFloat(samples_information_dict[group][lname][sample_name]) + 1);
@@ -244,7 +264,7 @@ function buildSamplesTable(samples_layer_order, samples_layers) {
let first_sample = Object.keys(samples_information_dict[group])[0];
for (let layer_name in samples_information_dict[group][first_sample]) {
let found = false;
-
+
for (const [index, entry] of all_information_layers.entries()) {
if (entry['group'] == group && entry['layer_name'] == layer_name) {
found = true;
@@ -284,12 +304,12 @@ function buildSamplesTable(samples_layer_order, samples_layers) {
var pretty_name = getNamedLayerDefaults(layer_name, 'pretty_name', layer_name);
pretty_name = (pretty_name.indexOf('!') > -1) ? pretty_name.split('!')[0] : pretty_name;
-
+
var short_name = (pretty_name.length > 10) ? pretty_name.slice(0,10) + "..." : pretty_name;
var hasSettings = false;
- if (typeof(samples_layers) !== 'undefined' &&
- typeof(samples_layers[group]) !== 'undefined' &&
+ if (typeof(samples_layers) !== 'undefined' &&
+ typeof(samples_layers[group]) !== 'undefined' &&
typeof(samples_layers[group][layer_name]) !== 'undefined') {
hasSettings = true;
layer_settings = samples_layers[group][layer_name];
@@ -298,8 +318,8 @@ function buildSamplesTable(samples_layer_order, samples_layers) {
if (isNumber(samples_information_dict[group][first_sample][layer_name]))
{
var data_type = "numeric";
-
- if (hasSettings)
+
+ if (hasSettings)
{
var norm = layer_settings['normalization'];
var min = layer_settings['min']['value'];
@@ -362,10 +382,10 @@ function buildSamplesTable(samples_layer_order, samples_layers) {
.replace(new RegExp('{max}', 'g'), max)
.replace(new RegExp('{margin}', 'g'), margin);
}
- else if (layer_name.indexOf(';') > -1)
+ else if (layer_name.indexOf(';') > -1)
{
var data_type = "stack-bar";
-
+
if (hasSettings)
{
var norm = layer_settings['normalization'];
@@ -411,7 +431,7 @@ function buildSamplesTable(samples_layer_order, samples_layers) {
else
{
var data_type = "categorical";
-
+
if (hasSettings)
{
var height = layer_settings['height'];
@@ -464,7 +484,7 @@ function buildSamplesTable(samples_layer_order, samples_layers) {
}
}).keyup(function() {
$(this).colpickSetColor(this.value);
- });
+ });
}
function drawSamples() {
@@ -499,7 +519,7 @@ function drawSamplesLayers(settings) {
var start = (samples_layer_settings['height'] == 0) ? 0 : samples_layer_settings['margin'];
var end = start + samples_layer_settings['height'];
-
+
if (i > 0)
{
start += samples_layer_boundaries[i-1][1];
@@ -562,7 +582,7 @@ function drawSamplesLayers(settings) {
}
samples_end = Math.max(layer_index);
- if (samples_layer_settings['data-type'] == 'numeric')
+ if (samples_layer_settings['data-type'] == 'numeric')
{
var value = _samples_information_dict[group][sample_name][samples_layer_name];
var min = parseFloat(samples_layer_settings['min']['value']);
@@ -609,7 +629,7 @@ function drawSamplesLayers(settings) {
rect.setAttribute('sample-group', group);
rect.setAttribute('layer-name', samples_layer_name);
}
- else if (samples_layer_settings['data-type'] == 'stack-bar')
+ else if (samples_layer_settings['data-type'] == 'stack-bar')
{
var norm = samples_layer_settings['normalization'];
var stack_bar_items = _samples_information_dict[group][sample_name][samples_layer_name].split(';');
@@ -743,9 +763,9 @@ function drawSamplesLayers(settings) {
drawText('samples', {
'x': layer_boundaries[samples_end][1] + 20,
'y': 0 - (samples_layer_boundaries[i][0] + samples_layer_boundaries[i][1]) / 2 + font_size / 6
- },
- getNamedLayerDefaults(samples_layer_name, 'pretty_name', samples_layer_name),
- font_size + 'px',
+ },
+ getNamedLayerDefaults(samples_layer_name, 'pretty_name', samples_layer_name),
+ font_size + 'px',
'left',
samples_layer_settings['color'],
'baseline');
@@ -767,8 +787,8 @@ function drawSamplesLayers(settings) {
drawText('samples', {
'x': layer_boundaries[samples_end][1] + 20,
'y': 0 - (samples_layer_boundaries[i][0] + samples_layer_boundaries[i][1]) / 2 + font_size / 6
- },
- getNamedLayerDefaults(samples_pretty_name, 'pretty_name', samples_pretty_name),
+ },
+ getNamedLayerDefaults(samples_pretty_name, 'pretty_name', samples_pretty_name),
font_size + 'px',
'left',
'#919191',
@@ -785,7 +805,7 @@ function drawSamplesLayers(settings) {
font_size + 'px',
'left',
samples_layer_settings['color'],
- 'baseline');
+ 'baseline');
}
}
}
diff --git a/anvio/data/interactive/js/search.js b/anvio/data/interactive/js/search.js
index 99b4af801a..d97fab239b 100644
--- a/anvio/data/interactive/js/search.js
+++ b/anvio/data/interactive/js/search.js
@@ -1,5 +1,22 @@
-
-function searchContigs()
+/**
+ * Search functions for the interactive interface.
+ *
+ * Authors: Ozcan Esen
+ *
+ * Copyright 2015-2021, The anvi'o project (http://anvio.org)
+ *
+ * Anvi'o is a free software. You can redistribute this program
+ * and/or modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation, either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with anvi'o. If not, see .
+ *
+ * @license GPL-3.0+
+ */
+
+function searchContigs()
{
var svalue = $('#searchValue').val();
@@ -11,7 +28,7 @@ function searchContigs()
var column = $('#searchLayerList').val();
search_column = (column == 0) ? 'Item Name' : layerdata[0][column];
var operator = $('#searchOperator').val();
-
+
if (operator < 6)
{
var operator_text = $('#searchOperator option:selected').text();
@@ -81,7 +98,7 @@ function searchFunctions() {
var _accession = data['results'][i][3];
var _annotation = data['results'][i][4];
var _search_term = data['results'][i][5];
- var _split_name = data['results'][i][6];
+ var _split_name = data['results'][i][6];
}
else
{
@@ -94,10 +111,10 @@ function searchFunctions() {
}
var _beginning = _annotation.toLowerCase().indexOf(_search_term.toLowerCase());
- _annotation = [_annotation.slice(0, _beginning),
- '',
- _annotation.slice(_beginning, _beginning + _search_term.length),
- ' ',
+ _annotation = [_annotation.slice(0, _beginning),
+ '',
+ _annotation.slice(_beginning, _beginning + _search_term.length),
+ ' ',
_annotation.slice(_beginning + _search_term.length, _annotation.length)
].join("");
@@ -196,11 +213,11 @@ function highlightResult() {
highlighted_splits.push(search_results[i]['split']);
}
- bins.HighlightItems(highlighted_splits);
+ bins.HighlightItems(highlighted_splits);
}
function highlightSplit(name) {
- bins.HighlightItems(name);
+ bins.HighlightItems(name);
}
function appendResult() {
diff --git a/anvio/data/interactive/js/structure.js b/anvio/data/interactive/js/structure.js
index a7f4f95a6f..61892cf2f1 100644
--- a/anvio/data/interactive/js/structure.js
+++ b/anvio/data/interactive/js/structure.js
@@ -1,3 +1,22 @@
+/**
+ * Javascript library to visualize gene structures
+ *
+ * Authors: Evan Kiefl
+ * Ozcan Esen
+ *
+ * Copyright 2015-2021, The anvi'o project (http://anvio.org)
+ *
+ * Anvi'o is a free software. You can redistribute this program
+ * and/or modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation, either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with anvi'o. If not, see .
+ *
+ * @license GPL-3.0+
+ */
+
const mode = 'structure';
const MAX_NGL_WIDGETS = 16;
@@ -82,8 +101,8 @@ $(document).ready(function() {
url: '/data/get_initial_data?timestamp=' + new Date().getTime(),
success: function(data) {
let available_gene_callers_ids = data['available_gene_callers_ids'];
- let available_engines = data['available_engines'];
- sample_groups = data['sample_groups'];
+ let available_engines = data['available_engines'];
+ sample_groups = data['sample_groups'];
available_gene_callers_ids.forEach(function(gene_callers_id) {
$('#gene_callers_id_list').append(`${gene_callers_id} `);
@@ -134,7 +153,7 @@ function load_sample_group_widget(category, trigger_create_ngl_views=true) {
-
${sample}
`;
@@ -175,7 +194,7 @@ function load_sample_group_widget(category, trigger_create_ngl_views=true) {
function apply_orientation_matrix_to_all_stages(orientationMatrix) {
for (let group in stages) {
- stages[group].viewerControls.orient(orientationMatrix);
+ stages[group].viewerControls.orient(orientationMatrix);
}
cached_orientation_matrices[$('#gene_callers_id_list').val()] = orientationMatrix;
}
@@ -217,8 +236,8 @@ async function create_single_ngl_view(group, num_rows, num_columns) {
var defer = $.Deferred();
$('#ngl-container').append(`
-
${group}
@@ -347,7 +366,7 @@ async function create_single_ngl_view(group, num_rows, num_columns) {
if (pickingProxy && pickingProxy.atom) {
if (pickingProxy.atom.resno != previous_hovered_residue && !(['always', 'variant residue'].includes($('#show_ballstick_when').val()))) {
- // remove ball+stick if hovered residue changed or
+ // remove ball+stick if hovered residue changed or
if (pickingProxy.atom.resno != previous_hovered_residue) {
stage.compList[0].reprList.slice(0).forEach((rep) => {
if (rep.name == 'ball+stick') {
@@ -373,7 +392,7 @@ async function create_single_ngl_view(group, num_rows, num_columns) {
for (i in contacts) {
if (contacts[i] == String(residue)) {
variant_contacts.push(contacts[i]);
- }
+ }
else if (variability[group].hasOwnProperty(parseInt(contacts[i]))) {
variant_contacts.push(contacts[i]);
}
@@ -930,7 +949,7 @@ function draw_variability() {
}
else
{
- spacefill_options['color'] = color_legend[engine][column][column_value];
+ spacefill_options['color'] = color_legend[engine][column][column_value];
}
} else {
spacefill_options['color'] = $('#color_static').attr('color');
@@ -1101,13 +1120,13 @@ function create_ui() {
$(container).append(`
-
-
+
@@ -1222,7 +1241,7 @@ function onTargetColumnChange(element) {
$(`#${prefix}_min`).val(selected_column_info['min']);
$(`#${prefix}_max`).val(selected_column_info['max']);
- }
+ }
else
{
$(`#${prefix}_numerical_panel`).hide();
@@ -1238,7 +1257,7 @@ function onTargetColumnChange(element) {
if (prefix == 'color') {
$(`#color_legend_panel`).append(`
-
' + state_name + '');
}
@@ -1799,7 +1818,7 @@ function showSaveStateWindow()
});
}
-function saveState()
+function saveState()
{
var name = $('#saveState_name').val();
diff --git a/anvio/data/interactive/js/svg-helpers.js b/anvio/data/interactive/js/svg-helpers.js
index c536a8ef3a..b5d1612001 100644
--- a/anvio/data/interactive/js/svg-helpers.js
+++ b/anvio/data/interactive/js/svg-helpers.js
@@ -1,11 +1,11 @@
/**
- * SVG drawing functions.
+ * Helper functions to draw SVG objects.
*
- * Author: Özcan Esen
- * Credits: A. Murat Eren
- * Copyright 2017, The anvio Project
+ * Authors: Ozcan Esen
+ * Matthew Klein
+ * A. Murat Eren
*
- * This file is part of anvi'o ().
+ * Copyright 2015-2021, The anvi'o project (http://anvio.org)
*
* Anvi'o is a free software. You can redistribute this program
* and/or modify it under the terms of the GNU General Public
diff --git a/anvio/data/interactive/js/tree.js b/anvio/data/interactive/js/tree.js
index 03f4c8d570..62013158fb 100644
--- a/anvio/data/interactive/js/tree.js
+++ b/anvio/data/interactive/js/tree.js
@@ -1,17 +1,17 @@
/**
* Javascript library to parse newick trees
*
- * Author: Özcan Esen
- * Credits: A. Murat Eren
- * Copyright 2015, The anvio Project
+ * Authors: Özcan Esen
+ * Matthew Klein
+ * A. Murat Eren
+ *
+ * Copyright 2015-2021, The anvi'o project (http://anvio.org)
*
- * This file is part of anvi'o ().
- *
* Anvi'o is a free software. You can redistribute this program
- * and/or modify it under the terms of the GNU General Public
- * License as published by the Free Software Foundation, either
+ * and/or modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation, either
* version 3 of the License, or (at your option) any later version.
- *
+ *
* You should have received a copy of the GNU General Public License
* along with anvi'o. If not, see .
*
@@ -330,7 +330,7 @@ Tree.prototype.Parse = function(str, edge_length_norm) {
i++;
if (isNumber(token[i])) {
curnode.original_edge_length = parseFloat(token[i]);
-
+
// normalization of edge lengths
if (edge_length_norm) {
curnode.edge_length = Math.sqrt(parseFloat(token[i]) * 1000000) / 1000000;
@@ -461,7 +461,7 @@ function NodeIterator(root)
}
-NodeIterator.prototype.Begin = function()
+NodeIterator.prototype.Begin = function()
{
/* if (this.root.constructor === Array)
{
@@ -478,7 +478,7 @@ NodeIterator.prototype.Begin = function()
};
-NodeIterator.prototype.Next = function()
+NodeIterator.prototype.Next = function()
{
/* if (this.root.constructor === Array)
{
@@ -522,14 +522,14 @@ function PreorderIterator()
};
-PreorderIterator.prototype.Begin = function()
+PreorderIterator.prototype.Begin = function()
{
this.cur = this.root;
return this.cur;
};
-PreorderIterator.prototype.Next = function()
+PreorderIterator.prototype.Next = function()
{
if (this.cur.child)
{
diff --git a/anvio/data/interactive/js/utils.js b/anvio/data/interactive/js/utils.js
index f575a40dab..227e3e63ae 100644
--- a/anvio/data/interactive/js/utils.js
+++ b/anvio/data/interactive/js/utils.js
@@ -1,10 +1,10 @@
/**
* Utility functions for anvi'o interactive interface
*
- * Author: Özcan Esen
- * Copyright 2015, The anvio Project
+ * Authors: Özcan Esen
+ * A. Murat Eren
*
- * This file is part of anvi'o ().
+ * Copyright 2015-2021, The anvi'o project (http://anvio.org)
*
* Anvi'o is a free software. You can redistribute this program
* and/or modify it under the terms of the GNU General Public
@@ -34,12 +34,70 @@ function is_large_angle(a, b) {
return (Math.abs(b - a) > Math.PI) ? 1 : 0;
}
+function clamp(num, min, max) {
+ return Math.min(Math.max(num, min), max);
+}
+
function info(step) {
// a funciton to keep user posted about what is going on.
timestamp = (new Date(Date.now())).toLocaleString().substr(11,7);
console.log(step + " (" + timestamp + ").");
}
+// https://stackoverflow.com/questions/9907419/how-to-get-a-key-in-a-javascript-object-by-its-value/36705765
+function getKeyByValue(object, value) {
+ return Object.keys(object).find(key => object[key] === value);
+}
+
+//-----------------------------------------------------------------------------
+// Gene function coloring
+//-----------------------------------------------------------------------------
+
+/*
+ * @returns target gene's category code for a given functional annotation type.
+ */
+function getCagForType(geneFunctions, fn_type) {
+ let out = geneFunctions != null && geneFunctions[fn_type] != null ? geneFunctions[fn_type][1] : null;
+ if(out && out.indexOf(',') != -1) out = out.substr(0,out.indexOf(',')); // take first cag in case of a comma-separated list
+ if(out && out.indexOf(';') != -1) out = out.substr(0,out.indexOf(';'));
+ if(out && out.indexOf('!!!') != -1) out = out.substr(0,out.indexOf('!!!'));
+ return out;
+}
+
+/*
+ * @returns target gene's category code for a given functional annotation type,
+ * given genomeID and geneID.
+ */
+function getCagForID(genomeID, geneID, fn_type) {
+ let funs = settings.genomeData.genomes.find(x => x[0]==genomeID)[1].genes.functions[geneID];
+ return getCagForType(funs, fn_type);
+}
+
+function appendColorRow(label, cagCode, color, prepend=false) {
+ let code = getCleanCagCode(cagCode);
+ var tbody_content =
+ ' \
+ \
+ \
+
\
+ \
+ ' + label + ' \
+ ';
+
+ if(prepend) {
+ $('#tbody_function_colors').prepend(tbody_content);
+ } else {
+ $('#tbody_function_colors').append(tbody_content);
+ }
+}
+
+function getCleanCagCode(code) {
+ if(!isNaN(code)) return code;
+ return code.split(' ').join('_').split('(').join('_').split(')').join('_').split(':').join('_').split('/').join('_').split('+').join('_').split('.').join('_').split('\'').join('_').split('\"').join('_');
+}
+
+//-----------------------------------------------------------------------------
+
function get_sequence_and_blast(item_name, program, database, target) {
$.ajax({
type: 'GET',
diff --git a/anvio/data/interactive/lib/fabric b/anvio/data/interactive/lib/fabric
new file mode 160000
index 0000000000..4861f7853c
--- /dev/null
+++ b/anvio/data/interactive/lib/fabric
@@ -0,0 +1 @@
+Subproject commit 4861f7853c663104b1dc98b6f1f0282fb7b76113
diff --git a/anvio/dbinfo.py b/anvio/dbinfo.py
index 24d78bc72e..14c9932b25 100644
--- a/anvio/dbinfo.py
+++ b/anvio/dbinfo.py
@@ -285,6 +285,13 @@ def __init__(self, path, *args, **kwargs):
DBInfo.__init__(self, path)
+class GenomeViewDBInfo(DBInfo):
+ db_type = 'genome-view'
+ hash_name = 'genome_view_db_hash'
+ def __init__(self, path, *args, **kwargs):
+ DBInfo.__init__(self, path)
+
+
class FindAnvioDBs(object):
"""A helper class to traverse a directory to find anvi'o databases.
@@ -383,4 +390,5 @@ def walk(self):
'pan': PanDBInfo,
'trnaseq': TRNADBInfo,
'modules': ModulesDBInfo,
+ 'genome-view': GenomeViewDBInfo,
}
diff --git a/anvio/dbops.py b/anvio/dbops.py
index 453092bf38..5d53e3e8c8 100644
--- a/anvio/dbops.py
+++ b/anvio/dbops.py
@@ -146,7 +146,7 @@ def __init__(self, args, r=run, p=progress):
# associated with the call. so having done our part, we will quietly return from here hoping
# that we are not driving a developer crazy somewhere by doing so.
D = lambda x: self.__dict__[x] if x in self.__dict__ else None
- if D('mode') == 'pan' or D('mode') == 'functional' or D('mode') == 'manual':
+ if D('mode') == 'pan' or D('mode') == 'functional' or D('mode') == 'manual' or D('mode') == 'genome-view':
return
A = lambda x: self.args.__dict__[x] if x in self.args.__dict__ else None
@@ -1289,7 +1289,7 @@ def gen_GFF3_file_of_sequences_for_gene_caller_ids(self, gene_caller_ids_list=[]
f"the parameter `--annotation-source`: {', '.join(self.a_meta['gene_function_sources'])}",
header="THE MORE YOU KNOW 🌈", lc='yellow')
- gene_functions_found = False
+ gene_functions_found = False
if gene_annotation_source and self.a_meta['gene_function_sources']:
if gene_annotation_source in self.a_meta['gene_function_sources']:
self.init_functions(requested_sources=[gene_annotation_source])
@@ -4632,6 +4632,78 @@ def disconnect(self):
self.db.disconnect()
+class GenomeViewDatabase:
+ """To create an empty genome view database and/or access one."""
+ def __init__(self, db_path, run=run, progress=progress, quiet=True):
+ self.db = None
+ self.db_path = db_path
+ self.db_type = 'genome-view'
+
+ self.run = run
+ self.progress = progress
+ self.quiet = quiet
+
+ self.init()
+
+
+ def init(self):
+ if not os.path.exists(self.db_path):
+ return
+
+ self.meta = dbi(self.db_path, expecting=self.db_type).get_self_table()
+
+ for key in []:
+ try:
+ self.meta[key] = int(self.meta[key])
+ except:
+ pass
+
+ self.genomes = set([s.strip() for s in self.meta['genomes'].split(',')])
+
+
+ # open the database
+ self.db = db.DB(self.db_path, anvio.__genome_view_db__version__)
+
+ self.run.info('Genome view database', 'An existing database, %s, has been initiated.' % self.db_path, quiet=self.quiet)
+ self.run.info('Genomes', self.meta['genomes'], quiet=self.quiet)
+
+
+ def touch(self):
+ """Creates an empty genome view database on disk, and sets `self.db` to access to it.
+ At some point self.db.disconnect() must be called to complete the creation of the new db."""
+
+ is_db_ok_to_create(self.db_path, self.db_type)
+
+ self.db = db.DB(self.db_path, anvio.__genome_view_db__version__, new_database=True)
+
+ # creating empty default tables
+ self.db.create_table(t.states_table_name, t.states_table_structure, t.states_table_types)
+
+ return self.db
+
+
+ def create(self, meta_values={}):
+ self.touch()
+
+ for key in meta_values:
+ self.db.set_meta_value(key, meta_values[key])
+
+ self.db.set_meta_value('creation_date', time.time())
+ self.db.set_meta_value('genome_view_db_hash', self.get_hash())
+
+ self.disconnect()
+
+ self.run.info('Genome view database', 'A new database, %s, has been created.' % (self.db_path), quiet=self.quiet)
+
+
+ def get_hash(self):
+ return 'hash' + str('%08x' % random.randrange(16**8))
+
+
+ def disconnect(self):
+ self.db.disconnect()
+
+
####################################################################################################
#
# TABLES
diff --git a/anvio/docs/__init__.py b/anvio/docs/__init__.py
index 3be1a10286..7453930422 100644
--- a/anvio/docs/__init__.py
+++ b/anvio/docs/__init__.py
@@ -448,6 +448,12 @@
"provided_by_anvio": True,
"provided_by_user": False
},
+ "genome-view": {
+ "name": "GENOME VIEW",
+ "type": "DISPLAY",
+ "provided_by_anvio": True,
+ "provided_by_user": False
+ },
"view-data": {
"name": "VIEW DATA",
"type": "TXT",
diff --git a/anvio/docs/artifacts/genome-view.md b/anvio/docs/artifacts/genome-view.md
new file mode 100644
index 0000000000..e56889c4b8
--- /dev/null
+++ b/anvio/docs/artifacts/genome-view.md
@@ -0,0 +1 @@
+An interactive anvi'o interface to study gene synteny across genomes.
diff --git a/anvio/genomedescriptions.py b/anvio/genomedescriptions.py
index 0b984b99f8..8a0926d069 100644
--- a/anvio/genomedescriptions.py
+++ b/anvio/genomedescriptions.py
@@ -869,6 +869,239 @@ def get_functions_and_sequences_dicts_from_contigs_db(self, metagenome_name, req
return (function_calls_dict, aa_sequences_dict, dna_sequences_dict)
+class AggregateGenomes(object):
+ """Aggregate information related to a group of genomes from anywhere.
+ The purpose of this class is to collect all relevant information about
+ a group of genomes comprehensively for downstream analyses. The primary
+ client of this class is genome view.
+ Please note that despite similar names, the use of this class and the class
+ AggregateFunctions is quite different.
+ """
+
+ def __init__(self, args, run=run, progress=progress, skip_init=False):
+ self.run = run
+ self.progress = progress
+
+ self.args = args
+ self.initialized = False
+
+ A = lambda x: args.__dict__[x] if x in args.__dict__ else None
+ self.external_genomes_file = A('external_genomes')
+ self.internal_genomes_file = A('internal_genomes')
+ self.pan_db_path = A('pan_db')
+
+ if self.pan_db_path:
+ utils.is_pan_db(self.pan_db_path)
+
+
+ self.genome_descriptions = GenomeDescriptions(args, progress=terminal.Progress(verbose=False))
+ self.genome_descriptions.load_genomes_descriptions()
+
+ # critical items for genome view bottleroutes:
+ self.genomes = {}
+ self.gene_associations = {}
+
+ # things to fill in optionally
+ self.continuous_data_layers = {'layers': [], 'data': {}}
+
+ # let's have this ready for convenience:
+ self.genome_names = list(self.genome_descriptions.genomes.keys())
+
+ if not skip_init:
+ self.init()
+
+
+ def init(self, populate_continuous_data=True):
+ """Learn everything about genomes of interest.
+ Calling this funciton will populate multiple critical dictionaries this class
+ designed to give access to, including `self.genomes` and `self.gene_associations`.
+ """
+
+ # before going through all the gneomes, we will recover gene associations. the reason
+ # we want to do it early on to make sure if there are incompatibilities between genome
+ # names of interest and reosurces provided to learn gene-gene associations (such as the
+ # pan database), they are discovered earlier than later:
+ self.gene_associations = self.get_gene_associations()
+
+ # now we will go through genomes of interest, and build our gigantic dictionary
+ for genome_name in self.genome_names:
+ self.genomes[genome_name] = {}
+
+ # learn all about genes:
+ self.genomes[genome_name]['genes'] = self.get_genes_dict(genome_name)
+
+ # learn all about contigs:
+ self.genomes[genome_name]['contigs'] = self.get_contigs_dict(genome_name)
+
+ self.initialized = True
+
+ if populate_continuous_data:
+ self.populate_genome_continuous_data_layers()
+
+
+ def get_gene_associations(self):
+ """Recovers gene assoctiations through gene clusters found in a pan database.
+ FIXME/TODO: also recover gene associations through user-provided input files.
+ """
+
+ d = {}
+
+ # if we have a pan database, we will start with that, first, and learn gene clusters in it:
+ if self.pan_db_path:
+ pan_db = dbops.PanDatabase(self.pan_db_path)
+
+ genome_names_missing_in_pan_db = [g for g in self.genome_names if g not in pan_db.genomes]
+ if len(genome_names_missing_in_pan_db):
+ raise ConfigError(f"You have provided a pan database to recover assocaitions between genes across "
+ f"your genomes, but not all genome names in your list of genomes occur in this "
+ f"pan database :/ Here is the list of genome names that you are missing: "
+ f"{', '.join(genome_names_missing_in_pan_db)}")
+ else:
+ self.run.warning("Anvi'o found each of the genome name you are interested in the pan database you "
+ "have provided. Which means the gene cluster information will be recovered for "
+ "downstream magic.", header="PAN DATABASE LOOKS GOOD 🚀", lc="green")
+
+ pan_db.disconnect()
+
+
+ pan_db = dbops.PanSuperclass(self.args)
+ pan_db.init_gene_clusters()
+
+ d['anvio-pangenome'] = {}
+ d['anvio-pangenome']['gene-cluster-name-to-genomes-and-genes'] = pan_db.gene_clusters
+ d['anvio-pangenome']['genome-and-gene-names-to-gene-clusters'] = pan_db.gene_callers_id_to_gene_cluster
+
+ return d
+
+
+ def get_genes_dict(self, genome_name):
+ """Learn everything about genes in a genome"""
+
+ contigs_db_path = self.genome_descriptions.genomes[genome_name]['contigs_db_path']
+
+ d = {}
+
+ # learn gene calls, start-stop positions, and so on
+ d['gene_calls'] = db.DB(contigs_db_path, None, ignore_version=True).smart_get(t.genes_in_contigs_table_name, 'gene_callers_id', self.genome_descriptions.genomes[genome_name]['gene_caller_ids'])
+
+ # learn gene functions as well as gene amino acid and DNA sequences
+ d['functions'], d['aa'], d['dna'] = self.genome_descriptions.get_functions_and_sequences_dicts_from_contigs_db(genome_name)
+
+ return d
+
+
+ def populate_continuous_GC_content_data(self):
+ """Add sliding window GC-content change per contig"""
+
+ if not self.initialized:
+ raise ConfigError("You can't populate continuous data layers unless the class is properly initialized.")
+
+ if 'GC_content' not in self.continuous_data_layers['layers']:
+ self.continuous_data_layers['layers'].append('GC_content')
+
+ self.progress.new('Populating continuous data', progress_total_items=len(self.genomes))
+ for genome_name in self.genomes:
+ self.progress.update(f"GC-content for {genome_name}", increment=True)
+
+ if genome_name not in self.continuous_data_layers['data']:
+ self.continuous_data_layers['data'][genome_name] = {}
+
+ self.continuous_data_layers['data'][genome_name]['GC_content'] = {}
+
+ for contig_name in self.genomes[genome_name]['contigs']['info']:
+ contig_sequence = self.genomes[genome_name]['contigs']['dna'][contig_name]['sequence']
+ self.continuous_data_layers['data'][genome_name]['GC_content'][contig_name] = utils.get_GC_content_for_sequence_as_an_array(contig_sequence)
+
+ self.progress.end()
+
+
+ def populate_genome_continuous_data_layers(self, skip_GC_content=False):
+ """Function to populate continuous data layers.
+ Calling this function will poupulate the `self.continuous_data_layers` data structure, which will
+ look something like this:
+ >>> {
+ >>> "layers": [
+ >>> "GC_content",
+ >>> "Another_layer"
+ >>> ],
+ >>> "data": {
+ >>> "GENOME_01": {
+ >>> "GC_content": {
+ >>> "CONTIG_A": [(...)],
+ >>> "CONTIG_B": [(...)],
+ >>> (...)
+ >>> },
+ >>> "Another_layer": {
+ >>> "CONTIG_A": [(...)],
+ >>> "CONTIG_B": [(...)],
+ >>> (...)
+ >>> }
+ >>> },
+ >>> "GENOME_02": {
+ >>> "GC_content": {
+ >>> "CONTIG_C": [(...)],
+ >>> "CONTIG_D": [(...)],
+ >>> (...)
+ >>> },
+ >>> "Another_layer": {
+ >>> "CONTIG_C": [(...)],
+ >>> "CONTIG_D": [(...)],
+ >>> (...)
+ >>> }
+ >>> },
+ >>> "GENOME_03": {
+ >>> "GC_content": {
+ >>> "CONTIG_E": [(...)],
+ >>> "CONTIG_F": [(...)],
+ >>> (...)
+ >>> },
+ >>> "Another_layer": {
+ >>> "CONTIG_E": [(...)],
+ >>> "CONTIG_F": [(...)],
+ >>> (...)
+ >>> }
+ >>> }
+ >>> }
+ >>> }
+ Please note: The number of data points in continuous data layers for a given contig will always
+ be equal or less than the number of nucleotides in the same contig. When displaying htese data,
+ one should always assume that the first data point matches to teh first nucleotide position, and
+ cut the information in the layer short when necessary.
+ """
+
+ if not self.initialized:
+ raise ConfigError("You can't populate continuous data layers unless the class is properly initialized.")
+
+ # reset in case it was previously populated
+ self.continuous_data_layers = {'layers': [], 'data': {}}
+
+ for genome_name in self.genomes:
+ self.continuous_data_layers['data'][genome_name] = {}
+
+ # add GC-content
+ if not skip_GC_content:
+ self.populate_continuous_GC_content_data()
+
+
+ def get_contigs_dict(self, genome_name):
+ """Learn everything about contigs associated with a genome"""
+
+ contigs_db_path = self.genome_descriptions.genomes[genome_name]['contigs_db_path']
+
+ d = {}
+
+ # get contig sequences
+ if genome_name in self.genome_descriptions.internal_genome_names:
+ raise ConfigError("This is not yet implemented :( Someone needs to find a rapid way to get contig "
+ "associated with a set of gene caller ids without having to create an instance "
+ "of ContigsSuperclass :/")
+ else:
+ d['info'] = db.DB(contigs_db_path, None, ignore_version=True).get_table_as_dict(t.contigs_info_table_name)
+ d['dna'] = db.DB(contigs_db_path, None, ignore_version=True).get_table_as_dict(t.contig_sequences_table_name)
+
+ return d
+
+
class AggregateFunctions:
"""Aggregate functions from anywhere.
diff --git a/anvio/interactive.py b/anvio/interactive.py
index c26ce211a9..e0792fbb9b 100644
--- a/anvio/interactive.py
+++ b/anvio/interactive.py
@@ -31,10 +31,10 @@
from anvio.variabilityops import VariabilitySuper
from anvio.variabilityops import variability_engines
from anvio.dbops import get_default_item_order_name
-from anvio.genomedescriptions import AggregateFunctions
+from anvio.genomedescriptions import AggregateFunctions, AggregateGenomes
from anvio.errors import ConfigError, RefineError, GenesDBError
from anvio.clusteringconfuguration import ClusteringConfiguration
-from anvio.dbops import ProfileSuperclass, ContigsSuperclass, PanSuperclass, TablesForStates, ProfileDatabase
+from anvio.dbops import ProfileSuperclass, ContigsSuperclass, PanSuperclass, TablesForStates, ProfileDatabase, GenomeViewDatabase
from anvio.tables.miscdata import (
TableForItemAdditionalData,
@@ -1849,6 +1849,73 @@ def get_anvio_news(self):
pass
+class GenomeView(AggregateGenomes):
+ def __init__(self, args, run=run, progress=progress, skip_init=False):
+ self.run = run
+ self.progress = progress
+
+ self.args = args
+ self.mode = 'genome-view'
+
+ A = lambda x: args.__dict__[x] if x in args.__dict__ else None
+ self.genome_view_db_path = A('genome_view_db')
+ self.tree = A('tree')
+ self.title = A('title')
+ self.skip_auto_ordering = A('skip_auto_ordering')
+ self.skip_news = A('skip_news')
+ self.dry_run = A('dry-run')
+
+ if not self.genome_view_db_path:
+ raise ConfigError("It is OK if you don't have a profile database, but you still should provide a "
+ "profile database path so anvi'o can generate a new one for you. The profile "
+ "database in this mode only used to read or store the 'state' of the display "
+ "for visualization purposes, or to allow you to create and store collections. "
+ "Still very useful.")
+
+ # critical items for genome view bottleroutes, which will be filled
+ # by `AggregateGenomes` upon initialization:
+ self.genomes = {}
+ self.initialized = False
+
+ if not skip_init:
+ AggregateGenomes.__init__(self, self.args, run=self.run, progress=self.progress)
+
+ self.init_genome_view_db()
+
+
+ def init_genome_view_db(self):
+ """Dealings with the genome view database.
+ This function will 'create' a genome view database if there is no such database
+ exists at the path provided. If there is one already, it will ensure
+ that it matches to the list of genomes aggregated during init.
+ """
+
+ if not len(self.genomes):
+ raise ConfigError("Someone asked anvi'o to initialize a genome view databse, but then "
+ "the class has aggregated zero genomes :( Something is wrong here.")
+
+ if os.path.exists(self.genome_view_db_path):
+ genome_view_db = GenomeViewDatabase(self.genome_view_db_path)
+
+
+ if not set(genome_view_db.genomes) == set(self.genomes.keys()):
+ raise ConfigError("The list of genomes names stored in your genome view database do not "
+ "match to the list of genome names anvi'o recovered from your input "
+ "files. If you have updated your input genomes for genome view, you "
+ "can remove this database and start over with a fresh one, or you can "
+ "simply provide a new genome view database path, so a new one matching "
+ "to your current list of genomes can be created.")
+ else:
+ genome_view_db = GenomeViewDatabase(self.genome_view_db_path)
+ genome_names = sorted(self.genomes.keys())
+ genome_view_db.create({'db_type': 'genome-view',
+ 'db_variant': None,
+ 'genomes': ','.join(genome_names)})
+
+ # states
+ self.states_table = TablesForStates(self.genome_view_db_path)
+
+
class StructureInteractive(VariabilitySuper, ContigsSuperclass):
def __init__(self, args, run=run, progress=progress):
self.run = run
diff --git a/anvio/tables/__init__.py b/anvio/tables/__init__.py
index c11cc54bf5..0200a94276 100644
--- a/anvio/tables/__init__.py
+++ b/anvio/tables/__init__.py
@@ -24,6 +24,7 @@
trnaseq_db_version = "2"
workflow_config_version = "3"
metabolic_modules_db_version = "4"
+genome_view_db_version = "1"
versions_for_db_types = {'contigs': contigs_db_version,
'profile': profile_db_version,
@@ -31,6 +32,7 @@
'structure': structure_db_version,
'pan': pan_db_version,
'genomestorage': genomes_storage_vesion,
+ 'genome-view': genome_view_db_version,
'auxiliary data for coverages': auxiliary_data_version,
'trnaseq': trnaseq_db_version,
'config': workflow_config_version,
diff --git a/anvio/tables/states.py b/anvio/tables/states.py
index 417723561a..a84308fe78 100644
--- a/anvio/tables/states.py
+++ b/anvio/tables/states.py
@@ -33,7 +33,7 @@ def __init__(self, db_path):
self.db_path = db_path
self.states = {}
- if utils.get_db_type(self.db_path) not in ['profile', 'pan', 'structure', 'genes']:
+ if utils.get_db_type(self.db_path) not in ['profile', 'pan', 'structure', 'genes', 'genome-view']:
raise ConfigError("Your database '%s' does not seem to have states table, which anvi'o tries to access.")
Table.__init__(self, self.db_path, utils.get_required_version_for_db(db_path), run, progress)
diff --git a/anvio/tests/run_component_tests_for_genome_view.sh b/anvio/tests/run_component_tests_for_genome_view.sh
new file mode 100755
index 0000000000..2783afa68b
--- /dev/null
+++ b/anvio/tests/run_component_tests_for_genome_view.sh
@@ -0,0 +1,18 @@
+#!/bin/bash
+source 00.sh
+set -e
+
+# Setup #############################
+SETUP_WITH_OUTPUT_DIR $1 $2
+#####################################
+
+INFO "Setting up the pan analysis directory"
+cp $files/mock_data_for_pangenomics/*.db $output_dir
+
+INFO "Generating an external genomes file for DBs"
+anvi-script-gen-genomes-file --input-dir $output_dir \
+ -o $output_dir/external-genomes.txt
+
+INFO "Running genome view"
+anvi-display-genomes -e $output_dir/external-genomes.txt \
+ -E $output_dir/genome-view.db
diff --git a/anvio/utils.py b/anvio/utils.py
index c0dd44a9d3..1c9d10e20d 100644
--- a/anvio/utils.py
+++ b/anvio/utils.py
@@ -1625,6 +1625,36 @@ def get_GC_content_for_sequence(sequence):
return Composition(sequence).GC_content
+def get_GC_content_for_sequence_as_an_array(sequence, sliding_window_length=100, interval=5):
+ """Unlike `get_GC_content_for_sequence`, this function returns an array for each nt position"""
+
+ if len(sequence) < sliding_window_length:
+ raise ConfigError("The sequence you are try to compute GC content for is shorter than the "
+ "sliding window length set for this calculation. This is not OK.")
+
+ if interval < 1 or interval > sliding_window_length:
+ raise ConfigError("Your interval can't be smaller than 1 or longer than the sliding window "
+ "length.")
+
+ sequence = sequence.upper()
+
+ gc_array = []
+ for i in range(0, len(sequence) - sliding_window_length, interval):
+ window = sequence[i:i+sliding_window_length]
+
+ gc = sum(window.count(x) for x in ["G", "C"])
+ at = sum(window.count(x) for x in ["A", "T"])
+
+ try:
+ gc_content = gc / (gc + at)
+ except ZeroDivisionError:
+ gc_content = 0.0
+
+ gc_array.extend([gc_content] * interval)
+
+ return gc_array
+
+
def get_synonymous_and_non_synonymous_potential(list_of_codons_in_gene, just_do_it=False):
"""
When calculating pN/pS or dN/dS, the number of variants classified as synonymous or non
diff --git a/bin/anvi-display-genomes b/bin/anvi-display-genomes
new file mode 100755
index 0000000000..75eb3e6a78
--- /dev/null
+++ b/bin/anvi-display-genomes
@@ -0,0 +1,91 @@
+#!/usr/bin/env python
+# -*- coding: utf-8
+"""Entry point to the interactive interface for genome view.
+
+The massage of the data is being taken care of in the interactive module,
+and this file implements the bottle callbacks.
+"""
+
+import sys
+from anvio.argparse import ArgumentParser
+
+import anvio
+import anvio.utils as utils
+import anvio.terminal as terminal
+import anvio.interactive as interactive
+from anvio.bottleroutes import BottleApplication
+
+from anvio.errors import ConfigError, FilesNPathsError
+
+
+__author__ = "Developers of anvi'o (see AUTHORS.txt)"
+__copyright__ = "Copyleft 2021, the Meren Lab (http://merenlab.org/)"
+__credits__ = []
+__license__ = "GPL 3.0"
+__version__ = anvio.__version__
+__maintainer__ = "A. Murat Eren"
+__email__ = "a.murat.eren@gmail.com"
+__requires__ = ['external-genomes', 'internal-genomes', 'pan-db']
+__provides__ = ['genome-view']
+__description__ = "Start an anvi'o interactive interface for 'genome view'"
+
+run = terminal.Run()
+progress = terminal.Progress()
+
+if __name__ == '__main__':
+ parser = ArgumentParser(description=__description__)
+
+ groupA = parser.add_argument_group('GENOMES', "Tell anvi'o where your genomes are.")
+ groupA.add_argument(*anvio.A('external-genomes'), **anvio.K('external-genomes'))
+ groupA.add_argument(*anvio.A('internal-genomes'), **anvio.K('internal-genomes'))
+
+ groupB = parser.add_argument_group('GENOME VIEW DB', "This is a database to store the display settings, bookmarks, etc. If "
+ "there is no database at the path specified, it will be AUTOMATICALLY generated.")
+ groupB.add_argument(*anvio.A('genome-view-db'), **anvio.K('genome-view-db', {'required': True}))
+
+ groupC = parser.add_argument_group('GENE ASSOCIATIONS', "Tell anvi'o how genes in your genomes relate to one another.")
+ groupC.add_argument(*anvio.A('pan-db'), **anvio.K('pan-db', {'required': False}))
+
+ groupD = parser.add_argument_group("PRO STUFF", "Tell anvi'o which gene caller to use.")
+ groupD.add_argument(*anvio.A('gene-caller'), **anvio.K('gene-caller'))
+
+ groupE = parser.add_argument_group('OPTIONAL INPUTS', "Where the yay factor becomes a reality.")
+ groupE.add_argument(*anvio.A('tree'), **anvio.K('tree'))
+
+ groupF = parser.add_argument_group('VISUALS RELATED', "Parameters that give access to various adjustements regarding\
+ the interface.")
+ groupF.add_argument(*anvio.A('title'), **anvio.K('title'))
+
+ groupG = parser.add_argument_group('SWEET PARAMS OF CONVENIENCE', "Parameters and flags that are not quite essential (but nice to have).")
+ groupG.add_argument(*anvio.A('dry-run'), **anvio.K('dry-run'))
+ groupG.add_argument(*anvio.A('skip-auto-ordering'), **anvio.K('skip-auto-ordering'))
+ groupG.add_argument(*anvio.A('skip-news'), **anvio.K('skip-news'))
+
+ groupH = parser.add_argument_group('SERVER CONFIGURATION', "For power users.")
+ groupH.add_argument(*anvio.A('ip-address'), **anvio.K('ip-address'))
+ groupH.add_argument(*anvio.A('port-number'), **anvio.K('port-number'))
+ groupH.add_argument(*anvio.A('browser-path'), **anvio.K('browser-path'))
+ groupH.add_argument(*anvio.A('read-only'), **anvio.K('read-only'))
+ groupH.add_argument(*anvio.A('server-only'), **anvio.K('server-only'))
+ groupH.add_argument(*anvio.A('password-protected'), **anvio.K('password-protected'))
+ groupH.add_argument(*anvio.A('user-server-shutdown'), **anvio.K('user-server-shutdown'))
+
+ args = parser.get_args(parser)
+
+ try:
+ d = interactive.GenomeView(args)
+
+ args.port_number = utils.get_port_num(args.port_number, args.ip_address, run=run)
+ except ConfigError as e:
+ print(e)
+ sys.exit(-1)
+ except FilesNPathsError as e:
+ print(e)
+ sys.exit(-2)
+
+ if args.dry_run:
+ run.info_single('Dry run? Kthxbai.', nl_after=1, nl_before=1)
+ sys.exit()
+
+ app = BottleApplication(d)
+ app.run_application(args.ip_address, args.port_number)
diff --git a/bin/anvi-self-test b/bin/anvi-self-test
index 671a9de58b..e98f1b4087 100755
--- a/bin/anvi-self-test
+++ b/bin/anvi-self-test
@@ -30,6 +30,7 @@ tests = {'mini' : ['run_component_tests_for_minimal_metagenomic
'metagenomics-full' : ['run_component_tests_for_metagenomics.sh'],
'pangenomics' : ['run_component_tests_for_pangenomics.sh'],
'interactive-interface' : ['run_component_tests_for_manual_interactive.sh'],
+ 'genome-view' : ['run_component_tests_for_genome_view.sh'],
'metabolism' : ['run_component_tests_for_metabolism.sh'],
'display-functions' : ['run_component_tests_for_display_functions.sh'],
'trnaseq' : ['run_component_tests_for_trnaseq.sh'],