7033f67074594c3d9cc3487f079456928fdf3260
angie
Tue Apr 12 13:46:14 2016 -0700
Major change to hgGateway: the contents are replaced by a new page designed by a graphic artist.
It has icons for selecting popular species, an autocomplete input for typing in species or common names,
as well as a phylogenetic tree display that shows the relationships of the species that we host.
It has a menu for selecting the assembly of the selected species' genome and the usual assembly
description.
refs #15277
diff --git src/hg/js/hgGateway.js src/hg/js/hgGateway.js
index b7dd63b..76a6401 100644
--- src/hg/js/hgGateway.js
+++ src/hg/js/hgGateway.js
@@ -1,30 +1,1547 @@
-$(document).ready(function() {
- suggestBox.init(document.orgForm.db.value,
- $('#suggestTrack').length > 0,
- function(item) {
- $('#positionDisplay').text(item.id);
- $('#position').val(item.id);
+// hgGateway - autocomplete + graphical interface to select species, assembly & position.
+
+// Copyright (C) 2016 The Regents of the University of California
+
+// Several modules are defined in this file -- if some other code needs them someday,
+// they can be moved out to lib files.
+
+// function svgCreateEl: convenience function for creating new SVG elements, used by
+// speciesTree and rainbow modules.
+
+// speciesTree: module that exports draw() function for drawing a phylogenetic tree
+// (and list of hubs above the tree, if any) in a pre-existing SVG element -- see
+// hg/hgGateway/hgGateway.html.
+
+// rainbow: module that exports draw() function and colors. draw() adds stripes using
+// a spectrum of colors that are associated to species groups. The hgGateway view code
+// uses coordinates of stripes within the tree image to create a corresponding "rainbow"
+// slider bar to the left of the phylogenetic tree container.
+
+// autocompleteCat: customized JQuery autocomplete plugin that includes watermark and
+// can display results broken down by category (for example, genomes from various
+// assembly hubs and native genomes).
+
+// hgGateway: module of mostly view/controller code (model state comes directly from server).
+
+// Globals:
+/* globals calculateHgTracksWidth */ // pragma for jshint; function is defined in utils.js
+var dbDbTree = dbDbTree || ['dbDbTree is missing!', []];
+var cart = cart || undefined;
+
+function svgCreateEl(type, config) {
+ // Helper function for creating a new SVG element and initializing its
+ // properties and attributes. Type is something like 'rect', 'text', 'g', etc;
+ // config is an object like { id: 'newThingie', x: 0, y: 10, title: 'blah blah' }.
+ var svgns = 'http://www.w3.org/2000/svg';
+ var xlinkns = 'http://www.w3.org/1999/xlink';
+ var el = document.createElementNS(svgns, type);
+ var title, titleEl;
+ if (el) {
+ _.forEach(config, function(value, setting) {
+ if (setting === 'textContent') {
+ // Text content (the text in a text element or title element) is a property:
+ el.textContent = value;
+ } else if (setting === 'href') {
+ // href comes from a different namespace so must use setAttributeNS:
+ el.setAttributeNS(xlinkns, 'href', value);
+ } else if (setting === 'title') {
+ title = value;
+ } else if (setting === 'className') {
+ el.setAttribute('class', value);
+ } else {
+ // Most of the time we're just setting an attribute:
+ el.setAttribute(setting, value);
+ }
+ });
+ }
+ // Mouseover title actually requires creating a child element.
+ // Strangely, if I did this in the above loop, the child element was lost if
+ // props/attributes were set afterwards!! So save title for last.
+ if (title) {
+ titleEl = document.createElementNS(svgns, 'title');
+ titleEl.textContent = title;
+ el.appendChild(titleEl);
+ }
+ return el;
+}
+
+///////////////////////////// Module: speciesTree /////////////////////////////
+
+var speciesTree = (function() {
+ // SVG phylogenetic tree of species in the browser (with connected assembly hubs above tree)
+
+ // Layout parameters/configuration (object passed into draw can override these defaults):
+ var cfg = { labelRightX: 230,
+ labelStartY: 18,
+ speciesLineOffsetX: 8,
+ branchLength: 5,
+ halfTextHeight: 4,
+ hubLineOffset: 100,
+ labelLineHeight: 18,
+ paddingRight: 5,
+ paddingBottom: 5,
+ branchPadding: 2,
+ onClickSpeciesName: 'console.log',
+ onClickHubName: 'console.log',
+ trackHubsUrl: '',
+ containerWidth: 370
+ };
+
+ function checkTree(node) {
+ // Return true if node and its descendants are of the form
+ // Array[ label:String, taxId:Number, sciName:String, Array[ [node]...] ]
+ // e.g. ['root', taxId0, sciName0,
+ // [ ['leaf1', taxId1, sciName1, []],
+ // ['leaf2', taxId2, sciName2, []] ]
+ // ]
+ if (! _.isString(node[0])) {
+ console.log('label is not a string', node);
+ return false;
+ }
+ if (! _.isNumber(node[1])) {
+ console.log('taxId is not a number', node);
+ return false;
+ }
+ if (! (node[2] === null || _.isString(node[2]))) {
+ console.log('sciName is not null or string', node);
+ return false;
+ }
+ return _.every(node[3], checkTree);
+ }
+
+ function addDepth(node) {
+ // Each node is of the form [ label, taxId, sciName, node[] ]
+ // e.g. ['root', taxId0, sciName0,
+ // [ ['leaf1', taxId1, sciName1, []],
+ // ['leaf2', taxId2, sciName2, []] ]
+ // ]
+ // Add a fifth property to node: depth, i.e. the maximum number of
+ // branching nodes along any path from node to a leaf.
+ // Returns depth.
+ var kids = node[3];
+ var depth, deepestKid;
+ if (!kids || kids.length === 0) {
+ // Leaf: depth is 0
+ depth = 0;
+ } else if (kids.length === 1) {
+ // Node with one child: pass-through, depth is child's depth
+ depth = addDepth(kids[0]);
+ } else {
+ // Node with multiple children: depth is 1 + max child depth
+ deepestKid = _.max(kids, addDepth);
+ depth = 1 + deepestKid[4];
+ }
+ node[4] = depth;
+ return depth;
+ }
+
+ function addSpeciesLabel(svg, label, taxId, sciName, y) {
+ // Add a species label to svg at y offset
+ var onClickString = cfg.onClickSpeciesName + '(' + taxId + ')';
+ var text = svgCreateEl('text', { x: cfg.labelRightX, y: y,
+ name: 'textEl_' + taxId,
+ title: sciName,
+ textContent: label,
+ onclick: onClickString });
+ svg.appendChild(text);
+ }
+
+ function addLine(svg, x1, y1, x2, y2, mouseover) {
+ // Add optionally with mouseover title
+ var config = { x1: x1, y1: y1,
+ x2: x2, y2: y2,
+ title: mouseover };
+ var line;
+ if (x1 === x2) {
+ // vertical lines get special styling
+ config.className = 'vert';
+ }
+ line = svgCreateEl('line', config);
+ svg.appendChild(line);
+ }
+
+ function calcBranchX(kidRetList, depth, parentDepth, parentNodeDepth) {
+ // Return the x offset used for drawing lines from children and a vertical line
+ // connecting them.
+ // If the branch from parent is longer than the minimum length, we can scale myX
+ // to reflect how many nodes are skipped in branches to the right vs. to the left,
+ // ensuring the minimum branch length on either side. Without this adjustment,
+ // we sometimes get very long branches that skip only one node to the right of short
+ // branches that skip many nodes.
+ var myX;
+ var kidMaxX = _.max(kidRetList, 'x').x;
+ var branchSteps = parentDepth - depth;
+ var kidMinNodeDepth, ratioSkipped, stepsX;
+ if (branchSteps > 1) {
+ kidMinNodeDepth = _.min(kidRetList, 'nodeDepth').nodeDepth;
+ ratioSkipped = kidMinNodeDepth / (kidMinNodeDepth + parentNodeDepth);
+ stepsX = (branchSteps+1) * ratioSkipped;
+ // Make sure there is at least one step (min branch width) before and after
+ // stepsX.
+ if (stepsX < 1) {
+ stepsX = 1;
+ } else if (stepsX > branchSteps) {
+ stepsX = branchSteps;
+ }
+ myX = kidMaxX + cfg.branchLength * stepsX;
+ } else {
+ // Only one change in depth level, so short branch:
+ myX = kidMaxX + cfg.branchLength;
+ }
+ return myX;
+ }
+
+ function drawNode(svg, node, leafY, leafTops, parentDepth, parentNodeDepth) {
+ // Each node is of the form [ label, taxId, sciName, node[], depth ]
+ // e.g. ['root', taxId0, sciName0,
+ // [ ['leaf1', taxId1, sciName1, [], 0],
+ // ['leaf2', taxId2, sciName2, [], 0] ],
+ // 1,
+ // ]
+ // depth is the max number of branching nodes under this node.
+ // Recursively draw the tree by generating new SVG elements and adding them to svg.
+ // Returns {x:, y:, leafY:} where x and y are the endpoint for the parent's branch
+ // to this node and leafY is the Y offset for the next leaf label.
+ // For leaf nodes, draw labels, store y in leafTops, and return label coordinates.
+ // Nodes with a single child don't draw anything, they simply return the
+ // child's info.
+ // Nodes with multiple children draw a horizontal line to each child and
+ // a vertical line connecting the horizontal lines. They return the {X,Y} of
+ // the midpoint of the vertical line.
+ // optional arg parentDepth is the depth of the parent node as defined above.
+ // optional arg parentNodeDepth is 1 plus the number of single-child nodes that were
+ // skipped on the way from the last branching ancestor.
+ // For internal use, the return object also includes nodeDepth: 1 + number of
+ // nodes skipped between this node and the next branching descendant or leaf
+ // and mouseover: horizontal line labels showing child label plus any skipped
+ // nodes' labels.
+ var label = node[0], taxId = node[1], sciName = node[2], kids = node[3], depth = node[4];
+ var myX, myY;
+ var kidRet, kidRetList, kidMinY, kidMaxY, extraSpace;
+ parentDepth = parentDepth || 1;
+ parentNodeDepth = parentNodeDepth || 1;
+ if (!kids || kids.length === 0) {
+ // leaf node: draw species label, store myY in leafTops
+ addSpeciesLabel(svg, label, taxId, sciName, leafY);
+ myX = cfg.labelRightX + cfg.speciesLineOffsetX;
+ myY = leafY - cfg.halfTextHeight;
+ leafTops[label] = leafY - cfg.labelLineHeight;
+ return { x: myX, y: myY, leafY: leafY + cfg.labelLineHeight, nodeDepth: 1 };
+ } else if (kids.length === 1) {
+ // Single child: don't draw anything (keep the rendering compact),
+ // but make a note that we skipped a node so we can use it for mouseover text
+ kidRet = drawNode(svg, kids[0], leafY, leafTops, parentDepth, parentNodeDepth + 1);
+ kidRet.nodeDepth++;
+ if (kidRet.mouseover) {
+ kidRet.mouseover += ' - ' + label;
+ } else {
+ kidRet.mouseover = label;
+ }
+ return kidRet;
+ } else {
+ // Multiple children. First pass to draw kids and gather their coords,
+ // second pass to draw lines connecting kids.
+ extraSpace = (depth - 1) * cfg.branchPadding;
+ kidRetList = _.map(kids, function(kid) {
+ var kidRet = drawNode(svg, kid, leafY, leafTops, depth, 1);
+ if (! kidRet.mouseover) {
+ // If nothing was skipped, use kid's sciName / label (same as kid's vert line)
+ var kidLabel = kid[0], kidSciName = kid[2];
+ kidRet.mouseover = kidSciName ? kidSciName : kidLabel;
+ }
+ leafY = kidRet.leafY + extraSpace;
+ return kidRet;
+ });
+ myX = calcBranchX(kidRetList, depth, parentDepth, parentNodeDepth);
+ // Draw horizontal lines from kids
+ _.forEach(kidRetList, function(kidRet) {
+ addLine(svg, kidRet.x, kidRet.y, myX, kidRet.y, kidRet.mouseover);
+ });
+ // Draw vertical line connecting kids
+ kidMinY = kidRetList[0].y;
+ kidMaxY = kidRetList[kids.length-1].y;
+ addLine(svg, myX, kidMinY, myX, kidMaxY, label);
+ myY = (kidMinY + kidMaxY) / 2;
+ return { x: myX, y: myY, leafY: leafY, nodeDepth: 1, mouseover: label };
+ }
+ }
+
+ function addTrackHubsLink(svg, x, y) {
+ // Add a label with link to hgHubConnect at the given position.
+ var a = svgCreateEl('a', { 'href': cfg.hgHubConnectUrl });
+ var text = svgCreateEl('text', { x: x, y: y,
+ textContent: 'Hub Genomes',
+ className: 'trackHubsLink' });
+ a.appendChild(text);
+ svg.appendChild(a);
+ }
+
+ function doubleQuote(string) {
+ return '"' + string + '"';
+ }
+
+ function addHubLabel(svg, hub, y) {
+ // Add a track hub label to svg at y offset
+ var label = hub.shortLabel + ' (' + hub.assemblyCount + ')';
+ // There are a bunch of hub properties to pass to the onClick handler;
+ // too bad we can't pass a bound function but instead must build a string:
+ var onClickString = cfg.onClickHubName + '(' +
+ doubleQuote(hub.hubUrl) + ', ' +
+ hub.taxId + ', ' +
+ doubleQuote(hub.defaultDb) + ', ' +
+ doubleQuote(hub.name) +
+ ')';
+ var text = svgCreateEl('text', { x: cfg.labelRightX, y: y,
+ textContent: label,
+ name: 'textEl_' + hub.name,
+ onclick: onClickString,
+ title: hub.longLabel });
+ svg.appendChild(text);
+ }
+
+ function drawHubs(svg, hubList, yIn) {
+ // Add a label for each hub in hubList, with a separator line below and
+ // "Hub Genomes" link instead of a tree.
+ var y = yIn;
+ var hub, i, textX, textY, lineX1, lineY, lineX2;
+ if (hubList && hubList.length) {
+ for (i = 0; i < hubList.length; i++) {
+ hub = hubList[i];
+ addHubLabel(svg, hub, y);
+ y += cfg.labelLineHeight;
+ }
+ textX = cfg.labelRightX + cfg.speciesLineOffsetX;
+ textY = (yIn + y - cfg.labelLineHeight) / 2;
+ addTrackHubsLink(svg, textX, textY);
+ lineX1 = cfg.hubLineOffset;
+ lineY = y - cfg.halfTextHeight;
+ lineX2 = cfg.containerWidth - cfg.speciesLineOffsetX;
+ addLine(svg, lineX1, lineY, lineX2, lineY);
+ y += cfg.labelLineHeight;
+ }
+ return y;
+ }
+
+ function draw(svg, dbDbTree, hubList, cfgOverrides) {
+ // dbDbTree is the root node of a phylogenetic tree that we render in svg.
+ // cfgOverrides should be used to provide (names of) meaningful onclick functions
+ // and track hub URL.
+ // Return the width and height of the tree, the top offset of the tree (below hubs
+ // if any), and an object with the top coords of each label/leaf.
+ // Instead of tacking a bunch of children directly onto svg, make a (group)
+ // and append that to svg when done.
+ // First see if there's already something there that we will replace:
+ var oldG = svg.getElementById('hubsAndTree');
+ var newG = svgCreateEl('g', { id: 'hubsAndTree' });
+ // y offsets of tops of species labels (leaves of dbDbTree), filled in by drawNode.
+ var leafTops = {};
+ var hubBottomY, treeInfo, width, height;
+ if (! checkTree(dbDbTree)) {
+ console.error('dbDbTree in wrong format', dbDbTree);
+ return;
+ }
+ _.assign(cfg, cfgOverrides);
+ hubBottomY = drawHubs(newG, hubList, cfg.labelStartY);
+ addDepth(dbDbTree);
+ treeInfo = drawNode(newG, dbDbTree, hubBottomY, leafTops);
+ width = treeInfo.x + cfg.paddingRight;
+ height = treeInfo.leafY - cfg.labelLineHeight + cfg.paddingBottom;
+ if (oldG) {
+ svg.removeChild(oldG);
+ }
+ svg.appendChild(newG);
+ return { width: width, height: height,
+ yTree: hubBottomY - cfg.labelLineHeight,
+ leafTops: leafTops };
+ }
+
+ return { draw: draw };
+
+}()); // speciesTree
+
+
+///////////////////////////// Module: rainbow /////////////////////////////
+
+var rainbow = (function() {
+ // Add rainbow stripes with cute species icons on the left of an SVG element that
+ // already has a phylogenetic tree drawn by speciesTree.
+
+ // Layout parameters/configuration (object passed into draw can override these defaults):
+ var cfg = { stripeWidth: 69,
+ iconX: 2,
+ iconYOffset: -14,
+ iconWidth: 65,
+ iconHeight: 65,
+ iconSpriteUrl: '../images/jWestIconsAlpha65px.png',
+ iconSpriteWidth: 325,
+ iconSpriteHeight: 325
+ };
+
+ // Color spectrum for slider and tree display:
+ var stripeColors = [ '#7E1F16',
+ '#A12321',
+ '#C15026',
+ '#DF6933',
+ '#EB8734',
+ '#B57E2A',
+ '#CD9C2A',
+ '#CFB32B',
+ '#959E38',
+ '#3A8349',
+ '#216D6D',
+ '#4C749B',
+ '#31469A',
+ '#7E4475',
+ '#231F1F' ];
+
+ // Hubs go above the species rainbow -- give them their own color of stripe:
+ var hubColor = '#60180C';
+
+ // Taxonomy IDs for assigning rainbow stripes to species groups:
+ var stripeTaxIds = [ 9443, // Primates
+ 314146, // Euarchontoglires
+ 9362, // Insectivora
+ 91561, // Cetartiodactyla
+ 314145, // Laurasiatheria
+ 9397, // Chiroptera
+ 9347, // Eutheria
+ 40674, // Mammalia
+ 32523, // Tetrapoda
+ 7742, // Vertebrata
+ 33511, // Deuterostomia
+ 33392, // Endopterygota
+ 6072, // Eumetazoa
+ 2759, // Eukaryota
+ 1 ]; // root
+
+ // Cute species icons placed along the stripe also help to orient.
+ // This maps leaf labels to icon names:
+ var iconSpeciesToName = { Human: 'Human',
+ Mouse: 'Mouse',
+ 'D. melanogaster': 'Fly',
+ 'C. elegans': 'Worm',
+ 'S. cerevisiae': 'Yeast',
+ 'Rhesus': 'Monkey',
+ Hedgehog: 'Hedgehog',
+ // Pig: 'Pig',
+ Cow: 'Cow',
+ 'Killer whale': 'Orca',
+ Horse: 'Horse',
+ Dog: 'Dog',
+ 'Pacific walrus': 'Walrus',
+ 'Megabat': 'Bat',
+ Elephant: 'Elephant',
+ Manatee: 'Manatee',
+ Armadillo: 'Armadillo',
+ 'Wallaby': 'Kangaroo',
+ 'Zebra finch': 'Bird',
+ Lizard: 'Lizard',
+ 'X. tropicalis': 'Frog',
+ 'Fugu': 'Fish',
+ 'Ebola virus': 'Ebola'
+ };
+
+ // The icon sprite image has 5 rows and 5 columns:
+ var spriteRowCol = { Human: [0,0],
+ Mouse: [0,1],
+ Fly: [0,3],
+ Worm: [0,4],
+ Yeast: [1,0],
+ Monkey: [1,1],
+ Hedgehog: [1,2],
+ Pig: [1,3],
+ Cow: [1,4],
+ Orca: [2,0],
+ Horse: [2,1],
+ Dog: [2,2],
+ Walrus: [2,3],
+ Bat: [2,4],
+ Elephant: [3,0],
+ Manatee: [3,1],
+ Armadillo: [3,2],
+ Kangaroo: [3,3],
+ Bird: [3,4],
+ Lizard: [4,0],
+ Frog: [4,1],
+ Fish: [4,2],
+ Ebola: [4,3]
+ };
+
+ // Some icon drawings are shorter than others, and some need to be moved up to make space
+ // for close neighbors.
+ var iconFudgeY = { Human: 20,
+ Mouse: 0,
+ Fly: 0,
+ Worm: 0,
+ Monkey: 0,
+ Hedgehog: -18,
+ Pig: -5,
+ Cow: 0,
+ Orca: 0,
+ Horse: -8,
+ Dog: 15,
+ Walrus: 0,
+ Bat: 0,
+ Elephant: -20,
+ Manatee: 20,
+ Armadillo: 0,
+ Kangaroo: 0,
+ Bird: 0,
+ Lizard: -20,
+ Frog: -10,
+ Fish: 0,
+ Yeast: -15,
+ Ebola: 0
+ };
+
+ function findStripeTops(node, parentStripeIx, leafTops, stripeTops, yPrev) {
+ // Each node is of the form [ label, taxId, sciName, node[], ... ]
+ // Recursively find the top coordinate of each stripe in stripeTaxIds,
+ // using node taxId. Modifies stripeTops. Returns the y of the top of the
+ // last leaf visited.
+ var label = node[0], taxId = node[1], kids = node[3];
+ var stripeIx = stripeTaxIds.indexOf(taxId);
+ var i;
+ // Inherit parent stripe unless this node is found in stripeTaxIds:
+ if (stripeIx < 0) {
+ stripeIx = parentStripeIx;
+ }
+ if (!kids || kids.length === 0) {
+ // leaf node: if this stripe's top coord has not yet been assigned,
+ // assign it.
+ if (stripeTops[stripeIx] === undefined) {
+ stripeTops[stripeIx] = (leafTops[label] + yPrev) / 2;
+ }
+ yPrev = leafTops[label];
+ } else {
+ // descend to children
+ for (i = 0; i < kids.length; i++) {
+ yPrev = findStripeTops(kids[i], stripeIx, leafTops, stripeTops, yPrev);
+ }
+ }
+ return yPrev;
+ }
+
+ function addRectFill(svg, x, y, width, height, color) {
+ // Add filled rectangle to svg
+ var rect = svgCreateEl('rect', { x: x, y: y,
+ width: width, height: height,
+ style: 'fill:' + color + '; stroke:' + color });
+ svg.appendChild(rect);
+ }
+
+ function drawStripes(svg, dbDbTree, yTop, height, leafTops) {
+ var stripeCount = stripeColors.length;
+ var lastStripeIx = stripeCount - 1;
+ var stripeTops = [];
+ var i, stripeHeight;
+ findStripeTops(dbDbTree, lastStripeIx, leafTops, stripeTops, yTop);
+ // Add an extra "stripe" coord so we have the coord for the bottom of the last stripe:
+ stripeTops[stripeCount] = height;
+ // Initialize missing stripes to 0-height (top = next stripe's top), if any:
+ for (i = stripeCount - 1; i >= 0; i--) {
+ if (stripeTops[i] === undefined) {
+ console.warn("No species found for stripe " + i + ", taxId " + stripeTaxIds[i]);
+ stripeTops[i] = stripeTops[i+1];
+ }
+ }
+ for (i = 0; i < stripeCount; i++) {
+ stripeHeight = stripeTops[i+1] - stripeTops[i];
+ addRectFill(svg, 0, stripeTops[i], cfg.stripeWidth, stripeHeight, stripeColors[i]);
+ }
+ // Add stripe for hubs, if any:
+ if (yTop > 0) {
+ addRectFill(svg, 0, 0, cfg.stripeWidth, yTop, hubColor);
+ }
+ return stripeTops;
+ }
+
+ function drawOneIcon(svg, name, y) {
+ // Create an image, offset so that the icon is positioned where we need it,
+ // and use a clip-path to limit display to just that icon, not the whole sprite image.
+ var iconY = y + cfg.iconYOffset + iconFudgeY[name];
+ var rowCol = spriteRowCol[name];
+ var row = rowCol[0], column = rowCol[1];
+ var clipPathId = 'clip' + name;
+ var img = svgCreateEl('image', { x: cfg.iconX - (column * cfg.iconWidth),
+ y: iconY - (row * cfg.iconHeight),
+ width: cfg.iconSpriteWidth,
+ height: cfg.iconSpriteHeight,
+ style: 'clip-path: url(#' + clipPathId + ')',
+ href: cfg.iconSpriteUrl });
+ // Set the y of the pre-existing clip path:
+ var rect = $('#' + clipPathId + ' rect')[0];
+ rect.setAttribute('y', iconY);
+ svg.appendChild(img);
+ }
+
+ function drawIcons(svg, leafTops) {
+ // For each icon listed in iconSpeciesToName, look up the species' y offset in
+ // the tree and add the icon to svg.
+ _.forEach(iconSpeciesToName, function (name, species) {
+ var y = leafTops[species];
+ if (y >= 0) {
+ drawOneIcon(svg, name, y);
+ }
+ });
+ }
+
+ function draw(svg, dbDbTree, yTree, height, leafTops) {
+ // Draw stripes with colors corresponding to species groups and cute-species icons.
+ // Return y offsets of stripes so that a slider widget can be drawn accordingly.
+ // Instead of tacking a bunch of children directly onto svg, make a (group)
+ // and append that to svg when done.
+ // First see if there's already something there that we will replace:
+ var oldG = svg.getElementById('stripesAndIcons');
+ var newG = svgCreateEl('g', { id: 'stripesAndIcons' });
+ var stripeTops = drawStripes(newG, dbDbTree, yTree, height, leafTops);
+ drawIcons(newG, leafTops);
+ if (oldG) {
+ svg.removeChild(oldG);
+ }
+ svg.appendChild(newG);
+ return stripeTops;
+ }
+
+ return { draw: draw,
+ colors: stripeColors,
+ hubColor: hubColor
+ };
+}()); // rainbow
+
+
+///////////////////////////// Module: autocompleteCat /////////////////////////////
+
+var autocompleteCat = (function() {
+ // Customize jQuery UI autocomplete to show item categories and support html markup in labels.
+ // Adapted from https://jqueryui.com/autocomplete/#categories and
+ // http://forum.jquery.com/topic/using-html-in-autocomplete
+ // Also adds watermarm to input.
+ $.widget("custom.autocompleteCat",
+ $.ui.autocomplete,
+ {
+ _renderMenu: function(ul, items) {
+ var that = this;
+ var currentCategory = "";
+ // There's no this._super as shown in the doc, so I can't override
+ // _create as shown in the doc -- just do this every time we render...
+ this.widget().menu("option", "items", "> :not(.ui-autocomplete-category)");
+ $.each(items,
+ function(index, item) {
+ // Add a heading each time we see a new category:
+ if (item.category && item.category !== currentCategory) {
+ ul.append("
" +
+ item.category + "
" );
+ currentCategory = item.category;
+ }
+ that._renderItem( ul, item );
+ });
},
- function(position) {
- $('#positionDisplay').text(position);
- $('#position').val(position);
+ _renderItem: function(ul, item) {
+ // In order to use HTML markup in the autocomplete, one has to overwrite
+ // autocomplete's _renderItem method using .html instead of .text.
+ // http://forum.jquery.com/topic/using-html-in-autocomplete
+ return $("")
+ .data("item.autocomplete", item)
+ .append($("").html(item.label))
+ .appendTo(ul);
+ }
});
- // Default the image width to current browser window width (#2633).
- var ele = $('input[name=pix]');
- if (ele.length && (!ele.val() || ele.val().length === 0)) {
- ele.val(calculateHgTracksWidth());
+ function init($input, options) {
+ // Set up an autocomplete and watermark for $input, with a callback options.onSelect
+ // for when the user chooses a result.
+ // If options.baseUrl is null, the autocomplete will not do anything, but we (re)initialize
+ // it anyway in case the same input had a previous db's autocomplete in effect.
+ // If options.searchObj is provided, it is used in addition to baseUrl; first the term is
+ // looked up in searchObj and then also queried using baseUrl. Values in searchObj
+ // should have the same structure as the value returned by a baseUrl query.
+ // The function closure allows us to keep a private cache of past searches.
+ var cache = {};
+
+ var doSearch = function(term, acCallback) {
+ // Look up term in searchObj and by sending an ajax request
+ var timestamp = new Date().getTime();
+ var url = options.baseUrl + encodeURIComponent(term) + '&_=' + timestamp;
+ var searchObjResults = [];
+ _.forEach(options.searchObj, function(results, key) {
+ if (_.startsWith(key.toUpperCase(), term.toUpperCase())) {
+ searchObjResults = searchObjResults.concat(results);
}
+ });
+ $.getJSON(url)
+ .done(function(results) {
+ var combinedResults = results.concat(searchObjResults);
+ cache[term] = combinedResults;
+ acCallback(combinedResults);
+ });
+ // ignore errors to avoid spamming people on flaky network connections
+ // with tons of error messages (#8816).
+ };
- if ($("#suggestTrack").length) {
- // Make sure suggestTrack is visible when user chooses something via gene select (#3484).
- $(document.mainForm).submit(function(event) {
- if ($('#hgFindMatches').length) {
- var track = $("#suggestTrack").val();
- if (track) {
- $("").appendTo($(event.currentTarget));
+ var autoCompleteSource = function(request, acCallback) {
+ // This is a callback for jqueryui.autocomplete: when the user types
+ // a character, this is called with the input value as request.term and an acCallback
+ // for this to return the result to autocomplete.
+ // See http://api.jqueryui.com/autocomplete/#option-source
+ var results = cache[request.term];
+ if (results) {
+ acCallback(results);
+ } else if (options.baseUrl) {
+ doSearch(request.term, acCallback);
}
+ };
+
+ var autoCompleteSelect = function(event, ui) {
+ // This is a callback for autocomplete to let us know that the user selected
+ // a term from the list. See http://api.jqueryui.com/autocomplete/#event-select
+ options.onSelect(ui.item);
+ $input.blur();
+ };
+
+ // Provide default values where necessary:
+ options.onSelect = options.onSelect || console.log;
+ options.searchObj = options.searchObj || {};
+ options.enterSelectsIdentical = options.enterSelectsIdentical || false;
+
+ $input.autocompleteCat({
+ delay: 500,
+ minLength: 2,
+ source: autoCompleteSource,
+ select: autoCompleteSelect,
+ enterSelectsIdentical: options.enterSelectsIdentical,
+ enterTerm: options.onEnterTerm
+ });
+
+ if (options.watermark) {
+ $input.Watermark(options.watermark, '#686868');
+ }
+ }
+
+ return { init: init };
+}()); // autocompleteCat
+
+
+///////////////////////////// Module: hgGateway /////////////////////////////
+
+var hgGateway = (function() {
+ // Interactive parts of the new gateway page: species autocomplete,
+ // graphical species-picker, db select, and position autocomplete.
+
+ // Constants
+ var speciesWatermark = 'Enter species or common name';
+ var positionWatermark = 'Enter position, gene symbol or search terms';
+ // Shortcuts to popular species:
+ var favIconTaxId = [ ['Human', 9606],
+ ['Mouse', 10090],
+ ['Rat', 10116],
+ ['Fly', 7227],
+ ['Worm', 6239],
+ ['Yeast', 559292] ];
+ // Aliases for species autocomplete:
+ var commonToSciNames = { bats: 'Chiroptera',
+ bees: 'Apoidea',
+ birds: 'Aves',
+ fish: 'Actinopterygii',
+ fly: 'Diptera',
+ flies: 'Diptera',
+ frogs: 'Anura',
+ fruitfly: 'Drosophila',
+ 'fruit fly': 'Drosophila',
+ honeybees: 'Apinae',
+ 'honey bees': 'Apinae',
+ monkeys: 'Simiiformes',
+ mosquitos: 'Culicidae',
+ worms: 'Nematoda',
+ yeast: 'Ascomycota' };
+
+ var getBetterBrowserMessage = '
' +
+ 'Our website has detected that you are using ' +
+ 'an outdated browser that will prevent you from ' +
+ 'accessing certain features. An update is not ' +
+ 'required, but it is strongly recommended to ' +
+ 'improve your browsing experience. ' +
+ 'Please use the following links to upgrade your ' +
+ 'existing browser to ' +
+ '' +
+ ' FireFox or ' +
+ '' +
+ 'Chrome.' +
+ '
';
+
+ // Globals
+ // Set this to true to see server requests and responses in the console.
+ var debugCartJson = false;
+ // This is a global (within wrapper function scope) so event handlers can use it
+ // without needing to bind functions.
+ var scrollbarWidth = 0;
+ // This holds everything we need to know to draw the page: taxId, db, hubs, description etc.
+ var uiState = {};
+
+ function setupFavIcons() {
+ // Set up onclick handlers for shortcut buttons and labels
+ var i, name, taxId, onClick;
+ for (i = 0; i < favIconTaxId.length; i++) {
+ name = favIconTaxId[i][0];
+ taxId = favIconTaxId[i][1];
+ // When user clicks on icon, set the taxId (default database);
+ // scroll the image to that species and clear the species autocomplete input.
+ onClick = setTaxId.bind(null, taxId, null, true, true);
+ // Onclick for both the icon and its sibling label:
+ $('.jwIconSprite' + name).parent().children().click(onClick);
+ }
+ }
+
+ function addCategory(cat, item) {
+ // Clone item, add category: cat to it and return it (helper function, see below).
+ var clone = {};
+ _.assign(clone, item, { category: cat });
+ return clone;
+ }
+
+ function autocompleteFromTree(node) {
+ // Traverse dbDbTree to make autocomplete result lists for all non-leaf node labels.
+ // Returns an object mapping each label of node and descendants to a list of
+ // result objects with the same structure that we'd get from a server request.
+ var searchObj = {};
+ var myResults = [];
+ var label = node[0], taxId = node[1], kids = node[3];
+ var addMyLabel;
+ if (!kids || kids.length === 0) {
+ // leaf node: return autocomplete result for species
+ myResults = [ { genome: label,
+ label: label,
+ taxId: taxId } ];
+ } else {
+ // Accumulate the list of all children's result lists;
+ // keep each's child searchObj mappings unless the child is a leaf
+ // (which would be redundant with server autocomplete results).
+ addMyLabel = addCategory.bind(null, label);
+ myResults = _.flatten(
+ _.map(kids, function(kid) {
+ var kidLabel = kid[0], kidKids = kid[3];
+ var kidObj = autocompleteFromTree(kid);
+ // Clone kid's result list and add own label as category:
+ var kidResults = _.map(kidObj[kidLabel], addMyLabel);
+ // Add kid's mappings to searchObj only if kid is not a leaf.
+ if (kidKids && kidKids.length > 0) {
+ _.assign(searchObj, kidObj);
+ }
+ return kidResults;
+ })
+ );
+ }
+ // Exclude some overly broad categories:
+ if (label !== 'root' && label !== 'cellular organisms') {
+ searchObj[label] = myResults;
+ }
+ return searchObj;
+ }
+
+ function addAutocompleteCommonNames(searchObj) {
+ // After searchObj is constructed by autocompleteFromTree, add aliases for
+ // some common names that map to scientific names in the tree.
+ _.forEach(commonToSciNames, function(sciName, commonName) {
+ var label, addMyLabel;
+ if (searchObj[sciName]) {
+ label = sciName + ' (' + commonName + ')';
+ addMyLabel = addCategory.bind(null, label);
+ searchObj[commonName] = _.map(searchObj[sciName], addMyLabel);
}
});
}
+
+ function makeStripe(id, color, stripeHeight, scrollTop, onClickStripe) {
+ // Return an empty div with specified background color and height
+ var $stripe = $('
');
+ $stripe.attr('id', 'rainbowStripe' + id);
+ $stripe.attr('title', 'Click to scroll the tree display');
+ $stripe.css('background-color', color);
+ $stripe.height(stripeHeight);
+ $stripe.click(onClickStripe.bind(null, scrollTop));
+ return $stripe;
+ }
+
+ function makeRainbowSliderStripes($slider, onClickStripe, svgHeight, stripeColors, stripeTops) {
+ // Set up the rainbow slider bar for the speciesPicker.
+ // The stripeColors array determines the number of stripes and their colors.
+ // stripeTops contains the pixel y coordinates of the top of each stripe;
+ // we convert these to pixel heights of HTML div stripes.
+ // onClickStripe is bound to the normalized top coord of each stripe
+ // (the ratio of stripe's top coord to slider bar height, in the range
+ // 0.0 to (1.0 - 1/stripeColors.length)).
+ var i, $stripe, scrollTop, stripeHeight, svgStripeHeight;
+ var sliderHeight = $('#speciesPicker').outerHeight();
+ $slider.empty();
+ if (stripeTops[0] > 0) {
+ // Add a placeholder stripe for hubs at the top
+ stripeHeight = sliderHeight * stripeTops[0] / svgHeight;
+ $stripe = makeStripe('Hub', rainbow.hubColor, stripeHeight, 0, onClickStripe);
+ $slider.append($stripe);
+ }
+ for (i = 0; i < stripeColors.length; i++) {
+ svgStripeHeight = stripeTops[i+1] - stripeTops[i];
+ stripeHeight = sliderHeight * svgStripeHeight / svgHeight;
+ scrollTop = stripeTops[i] / svgHeight;
+ $stripe = makeStripe(i, stripeColors[i], stripeHeight, scrollTop, onClickStripe);
+ $slider.append($stripe);
+ }
+ }
+
+ function resizeSliderIcon($sliderIcon, svgHeight, sliderBarHeight) {
+ // Make the slider icon's height cover the same portion of the slider bar
+ // as the visible part of the tree is compared to the entire tree image height.
+ var visiblePortion = _.min([1, sliderBarHeight / svgHeight]);
+ var iconHeight = visiblePortion * sliderBarHeight;
+ // Set the icon rectangle's height and triangle vertical offset.
+ var svg = document.getElementById('sliderSvg');
+ var rect = document.getElementById('sliderRectangle');
+ var tri = document.getElementById('sliderTriangle');
+ var strokeWidth = 3, triangleHeight = 6;
+ svg.setAttribute('height', iconHeight);
+ rect.setAttribute('height', iconHeight - strokeWidth);
+ tri.setAttribute('d', 'm 2.5,' + ((iconHeight - triangleHeight) / 2) + ' 0,6 4,-3 z');
+ $sliderIcon.height(iconHeight);
+ }
+
+ function initRainbowSlider(svgHeight, stripeColors, stripeTops) {
+ // Once we know the height of the hubs & tree image, initialize the rainbow slider
+ // widget for coordinated scrolling. Dragging the slider causes the image to scroll.
+ // Scrolling the image causes the slider to move. Clicking on a stripe causes the
+ // image to scroll and the slider to move.
+ var $speciesPicker = $('#speciesPicker');
+ var $sliderBar = $('#rainbowSlider');
+ var sliderBarTop = $sliderBar.offset().top;
+ var sliderBarHeight = $speciesPicker.outerHeight();
+ var $sliderIcon = $('#sliderSvgContainer');
+ var sliderIconLeft = $sliderIcon.offset().left;
+ var $speciesTree = $('#speciesTree');
+ // When the user moves the slider, causing the image to scroll, don't do the
+ // image onscroll action (do that only when the user scrolls the image).
+ var inhibitImageOnScroll = false;
+ // Don't let the slider hang off the bottom when the user clicks the bottom stripe:
+ var maxNormalizedTop = 1 - (sliderBarHeight / svgHeight);
+
+ // Define several helper functions within this function scope so they can use
+ // the variables defined above.
+ var scrollImage = function(normalizedTop) {
+ // Scroll the hubs+tree image to a normalized top coord scaled by svgHeight.
+ $speciesPicker.scrollTop(svgHeight * normalizedTop);
+ };
+
+ var moveSlider = function(normalizedTop) {
+ // Move the slider icon to a normalized top coord scaled by sliderBarHeight.
+ $sliderIcon.offset({ top: sliderBarTop + (normalizedTop * sliderBarHeight),
+ left: sliderIconLeft.left });
+ };
+
+ var onClickStripe = function(normalizedTop) {
+ // The user clicked a stripe; move the slider to the top of that stripe and
+ // scroll the tree image to the top of the corresponding stripe in the image.
+ inhibitImageOnScroll = true;
+ if (normalizedTop > maxNormalizedTop) {
+ normalizedTop = maxNormalizedTop;
+ }
+ scrollImage(normalizedTop);
+ moveSlider(normalizedTop);
+ };
+
+ var onDragSlider = function(event, ui) {
+ // The user dragged the slider; scroll the tree image to the corresponding
+ // position.
+ var sliderTop = ui.offset.top - sliderBarTop;
+ var normalizedTop = sliderTop / sliderBarHeight;
+ inhibitImageOnScroll = true;
+ scrollImage(normalizedTop);
+ };
+
+ var onScrollImage = function() {
+ // The user scrolled the image -- or the user did something else which caused
+ // the image to scroll, in which case we don't need to do anything more.
+ var imageTop, normalizedTop;
+ if (inhibitImageOnScroll) {
+ inhibitImageOnScroll = false;
+ return;
+ }
+ imageTop = -$speciesTree.offset().top + sliderBarTop + 1;
+ normalizedTop = imageTop / svgHeight;
+ moveSlider(normalizedTop);
+ };
+
+ // This might be called before the species image has been created; if so, do nothing.
+ if (! $speciesTree || speciesTree.length === 0) {
+ return;
+ }
+
+ makeRainbowSliderStripes($sliderBar, onClickStripe, svgHeight, stripeColors, stripeTops);
+ resizeSliderIcon($sliderIcon, svgHeight, sliderBarHeight);
+ $sliderIcon.draggable({ axis: 'y',
+ containment: '#speciesGraphic',
+ drag: onDragSlider
+ });
+ $sliderIcon.show();
+ $speciesPicker.scroll(onScrollImage);
+ }
+
+ function findScrollbarWidth() {
+ var widthPlain = $("#sbTestContainerDPlain").width();
+ var widthInsideScroll = $("#sbTestContainerDInsideScroll").width();
+ $('#sbTestContainer').hide();
+ return widthPlain - widthInsideScroll;
+ }
+
+ function setRightColumnWidth() {
+ // Adjust the width of the "Find Position" section so it fits to the right of the
+ // "Browse/Select Species" section.
+ var ieFudge = scrollbarWidth ? scrollbarWidth + 4 : 0;
+ var extraFudge = 4;
+ var rightColumnWidth = ($('#pageContent').width() -
+ $('#selectSpeciesSection').width() -
+ ieFudge - extraFudge);
+ if (rightColumnWidth >= 400) {
+ $('#findPositionSection').width(rightColumnWidth);
+ }
+ }
+
+ function setSpeciesPickerSizes(svgWidth, svgHeight) {
+ // Adjust widths and heights of elements in #speciesPicker according to svg size.
+ $('#speciesTree').width(svgWidth);
+ $('#speciesTree').height(svgHeight);
+ $('#speciesTreeContainer').height(svgHeight);
+ // Make #speciesTreeContainer skinnier if a scrollbar is taking up space
+ // in #speciesPicker.
+ var leftover = ($("#speciesPicker").width() - scrollbarWidth);
+ $("#speciesTreeContainer").width(leftover);
+ }
+
+ function highlightLabel(selectedName, scrollToItem) {
+ // Highlight the selected species.
+ // jQuery (at least our old version of it) can find the SVG text elements but can't
+ // directly manipulate their class, so do it manually.
+ var $sp = $('#speciesPicker');
+ var y = $sp.scrollTop();
+ $('svg text').each(function(ix, el) {
+ var elName = el.getAttribute('name');
+ var elClass = el.getAttribute('class');
+ if (!elClass || elClass.indexOf('trackHubsLabel') < 0) {
+ if (elName === selectedName) {
+ el.setAttribute('class', 'selected');
+ y = el.getAttribute('y');
+ } else if (elClass === 'selected') {
+ el.setAttribute('class', '');
+ }
+ }
});
+ if (scrollToItem) {
+ $sp.scrollTop(y - 100);
+ }
+ }
+
+ function hubNameFromDb(db) {
+ var matches = db ? db.match(/^(hub_[0-9]+)_/) : null;
+ if (matches) {
+ return matches[1];
+ } else {
+ return null;
+ }
+ }
+
+ function highlightLabelForDb(db, taxId) {
+ var hubName = hubNameFromDb(db);
+ if (hubName) {
+ highlightLabel('textEl_' + hubName, true);
+ } else {
+ highlightLabel('textEl_' + taxId, true);
+ }
+ }
+
+ function drawSpeciesPicker() {
+ var svg, spTree, stripeTops;
+ if (document.createElementNS) {
+ // Draw the phylogenetic tree and do layout adjustments
+ svg = document.getElementById('speciesTree');
+ spTree = speciesTree.draw(svg, dbDbTree, uiState.hubs,
+ { onClickSpeciesName: 'hgGateway.onClickSpeciesLabel',
+ onClickHubName: 'hgGateway.onClickHubName',
+ hgHubConnectUrl: 'hgHubConnect?hgsid=' + window.hgsid,
+ containerWidth: $('#speciesPicker').width()
+ });
+ setSpeciesPickerSizes(spTree.width, spTree.height);
+ highlightLabelForDb(uiState.db, uiState.taxId);
+ stripeTops = rainbow.draw(svg, dbDbTree, spTree.yTree, spTree.height, spTree.leafTops);
+ initRainbowSlider(spTree.height, rainbow.colors, stripeTops);
+ } else {
+ $('#speciesTreeContainer').html(getBetterBrowserMessage);
+ }
+ }
+
+ function onSelectGene(item) {
+ // Set the position from an autocomplete result;
+ // set hgFindMatches and make sure suggestTrack is in pack mode for highlighting the match.
+ var newPos = item.id;
+ var settings;
+ $('#positionDisplay').text(newPos);
+ if (uiState.suggestTrack) {
+ settings = { 'hgFind.matches': item.internalId };
+ settings[uiState.suggestTrack] = 'pack';
+ cart.send({ cgiVar: settings });
+ cart.flush();
+ }
+ // Overwrite the selected item w/actual position after the autocomplete plugin is done:
+ function overwriteWithPos() {
+ $('#positionInput').val(newPos);
+ }
+ window.setTimeout(overwriteWithPos, 0);
+ }
+
+ function updateGoButtonPosition() {
+ // If there's room for the go button to appear to the right of #selectAssembly and
+ // #positionInput, align the top of the go button with the bottom of #selectAssembly.
+ // Otherwise let it hang out below.
+ var $fpic = $('#findPosInputContainer');
+ var fpicOffset = $fpic.offset();
+ var fpicRight = fpicOffset.left + $fpic.width();
+ var $button = $('.jwGoButtonContainer');
+ var buttonOffset = $button.offset();
+ var $select;
+ if (buttonOffset.left > fpicRight) {
+ // Align button top with select bottom.
+ $select = $('#selectAssembly');
+ buttonOffset.top = $select.offset().top + $select.height();
+ } else {
+ // Button wraps around to below inputs; remove any previous vertical offsetting.
+ buttonOffset.top = fpicOffset.top + $fpic.height() + 10;
+ }
+ $button.offset(buttonOffset);
+ }
+
+ function setAssemblyOptions(uiState) {
+ var assemblySelectLabel = 'Assembly';
+ if (uiState.dbOptions) {
+ var html = '', option, i, selected;
+ for (i = 0; i < uiState.dbOptions.length; i++) {
+ option = uiState.dbOptions[i];
+ selected = (option.value === uiState.db) ? 'selected ' : '';
+ html += '';
+ }
+ $('#selectAssembly').html(html);
+ }
+ if (uiState.genomeLabel) {
+ if (uiState.hubUrl && uiState.genomeLabel.indexOf('Hub') < 0) {
+ assemblySelectLabel = uiState.genomeLabel + ' Hub Assembly';
+ } else {
+ assemblySelectLabel = uiState.genomeLabel + ' Assembly';
+ }
+ }
+ $('#selectAssemblyLabel').text(assemblySelectLabel);
+ }
+
+ function trackHubSkipHubName(name) {
+ // Just like hg/lib/trackHub.c's...
+ var matches;
+ if (name && (matches = name.match(/^hub_[0-9]+_(.*)/)) !== null) {
+ return matches[1];
+ } else {
+ return name;
+ }
+ }
+
+ function setAssemblyDescriptionTitle(db, genome) {
+ $('#descriptionGenome').html(trackHubSkipHubName(genome));
+ $('#descriptionDb').html(trackHubSkipHubName(db));
+ }
+
+ function insertNamedAnchor(string, ix, anchorName) {
+ return string.substring(0, ix) + '' + string.substring(ix);
+ }
+
+ function linkToNamedAnchor(title, anchor) {
+ return '' +
+ ' ' +
+ title + ' ';
+ }
+
+ function addSubsectionLink(section, title, anchor) {
+ var ix = section.bottom.indexOf(title);
+ if (ix >= 0) {
+ section.bottom = insertNamedAnchor(section.bottom, ix, anchor);
+ section.anchors += linkToNamedAnchor(title, anchor);
+ }
+ }
+
+ function digestDescription(description) {
+ // Search for familiar patterns in our description.html text and if possible,
+ // break it into a top (summary and photo), anchor section (links to useful
+ // subsections of details), and bottom (details).
+ var section = { top: '', anchors: '', bottom: ''};
+ var re, matches;
+ // IE8 can't handle the regex syntax [^] ("not the null character") which matches newlines
+ // in later versions. So wrap this in a try/catch:
+ try {
+ re = new RegExp('([^]*?)([^]*?Search the assembly[^]*)');
+ if (description) {
+ matches = description.match(re);
+ if (matches) {
+ section.top = matches[1];
+ section.bottom = matches[2];
+ } else {
+ matches = description.match(/([^]*?)(