ad2c455833e131ab3fcfb22889803e88f6cadb96 kate Wed Nov 9 17:03:34 2016 -0800 Major JS code cleanup and refactor. Also fixed some event handling. refs #17369 diff --git src/hg/js/hgGtexTrackSettings.js src/hg/js/hgGtexTrackSettings.js index ceb18c9..e78edc4 100644 --- src/hg/js/hgGtexTrackSettings.js +++ src/hg/js/hgGtexTrackSettings.js @@ -1,308 +1,350 @@ -// gtexTrackSettings - Interactive features for GTEX Body Map version of GTEx Gene track UI page +// gexTrackSettings - Interactive features for GTEX Body Map version of GTEx Gene track UI page // Copyright (C) 2016 The Regents of the University of California -// This file contains a single module that encapsulates all data and functions used to manage -// the hgGtexTrackSettings page. - +// This file contains a single module that encapsulates all javascript data and functions used to manage +// the hgGtexTrackSettings page, which is initially generated by the hgGtexTrackSettings CGI. +// The page contains an SVG format illustration of a human body with tissues, +// and a companion list of the same tissues. The tissues are based on the GTEx Project, and total 53. +// +// Users can interact with either the illustration or the list, and any selections they make will be +// reflected in the other. The interactions are click (select/unselect) or hover (highlight the tissue +// in the body map). There are 53 total tissues -- the GTEx Consortium set. Tissue abbreviations and +// colors used in the UI are from the hgFixed.gtexTissue table. +// +// Each tissue is represented in the SVG by: +// 1. shape <path id=$tissue_Pic_Lo> +// 2. highlighted shape <path id=$tissue_Pic_Hi> +// 3. highlight surround <path id=$tissue_Aura_Hi> +// 4. label <text id=$tissue_Text_Hi> +// 5. leader line <line | polyline id=$tissue_Lead_Hi> +// +// Implementation note: jQuery is used below where effective. Some SVG manipulation (e.g. add/remove/toggle +// classes) don't work from jQuery, so code for that is raw javascript var gtexTrackSettings = (function() { // Globals - var _htmlDoc; - var _htmlRoot; var _svgDoc; // SVG has its own DOM var _svgRoot; - var _topTissue; + + var _topTissue; // Highlighted tissue is drawn last so it is on top var _topAura; + var TEXT_HI = '_Text_Hi'; + var LEAD_HI = '_Lead_Hi'; + var AURA_HI = '_Aura_Hi'; + var PIC_HI = '_Pic_Hi'; + var PIC_LO = '_Pic_Lo'; + + var COLOR_BLACK = 'black'; + var COLOR_SELECTED = COLOR_BLACK; + var COLOR_BLUE = 'blue'; + var COLOR_HIGHLIGHT = COLOR_BLUE; + var COLOR_GRAY = '#737373'; + var COLOR_UNSELECTED = COLOR_GRAY; + var COLOR_PINK = '#F69296'; + var COLOR_LEADER = COLOR_PINK; + + var CLASS_TISSUE_SELECTED = 'tissueSelected'; + var CLASS_TISSUE_HOVERED = 'tissueHovered'; + + // 53 tissues from GTEx, as in hgTracks.gtexTissue table + // TODO: Consider generating this list during make, to an auxiliary .js file var tissues = [ 'adiposeSubcut', 'adiposeVisceral', 'adrenalGland', 'arteryAorta', 'arteryCoronary', 'arteryTibial', 'bladder', 'brainAmygdala', 'brainAnCinCortex', 'brainCaudate', 'brainCerebelHemi', 'brainCerebellum', 'brainCortex', 'brainFrontCortex', 'brainHippocampus', 'brainHypothalamus', 'brainNucAccumbens', 'brainPutamen', 'brainSpinalcord', 'brainSubstanNigra', 'breastMamTissue', 'xformedlymphocytes', 'xformedfibroblasts', 'ectocervix', 'endocervix', 'colonSigmoid', 'colonTransverse', 'esophagusJunction', 'esophagusMucosa', 'esophagusMuscular', 'fallopianTube', 'heartAtrialAppend', 'heartLeftVentricl', 'kidneyCortex', 'liver', 'lung', 'minorSalivGland', 'muscleSkeletal', 'nerveTibial', 'ovary', 'pancreas', 'pituitary', 'prostate', 'skinNotExposed', 'skinExposed', 'smallIntestine', 'spleen', 'stomach', 'testis', 'thyroid', 'uterus', 'vagina', 'wholeBlood' ]; // Convenience functions function tissueFromSvgId(svgId) { // Get tissue name from an SVG id. Convention here is <tis>_* return svgId.split('_')[0]; } - function initTissue(tis) { - // Set tissue to unhighlighted state - $("#" + tis + "_Pic_Hi", _svgRoot).hide(); - - // Mark tissue labels in svg - var el = _svgDoc.getElementById(tis + "_Text_Hi"); - if (el !== null) { - el.classList.add('tissueLabel'); - } - $("#" + tis + "_Aura_Hi", _svgRoot).hide(); - } - - function initBodyMap(svg, doc) { - // Set organs to unhighlighted state - tissues.forEach(initTissue); - } - - function onClickToggleTissue(tis) { - // mark selected in tissue table - tis = this.id; // arg bad - $(this).toggleClass('tissueSelected'); - - // jQuery addClass doesn't work on SVG elements, using classList - // May need a shim so this works on IE9 and Opera mini (as of Dec 2014) - // https://martinwolf.org/blog/2014/12/adding-and-removing-classes-from-svg-elements-with-jquery - var el = _svgDoc.getElementById(this.id + '_Text_Hi'); - if (el !== null) { - el.classList.toggle('tissueSelected'); - if ($(this).hasClass('tissueSelected')) { - onClickSetTissue(tis); - } else { - onClickClearTissue(tis); + function setMapTissueElColor(el) { + // Change appearance of label in body map. This function is part of setTissue(), + // used at initialization time (when other element attributes are already set by CGI) + // NOTE: label may be consist of multiple text elements, so traverse children + // TODO: Try replacing with CSS (First attempt resulted in black only after mouseover!) + if (el === null) { + return; } + el.style.fill = COLOR_SELECTED; + var count = el.childElementCount; + for (var i = 0; i < count; i++) { + el.children[i].style.fill = COLOR_SELECTED; } } - function onMapClickToggleTissue(ev) { - var svgId = ev.target.id; - var el = _svgDoc.getElementById(svgId); - if (el !== null) { - el.classList.toggle('tissueSelected'); - } - var tis = tissueFromSvgId(svgId); - el = _htmlDoc.getElementById(tis); - if (el !== null) { - el.classList.toggle('tissueSelected'); - if (el.classList.contains('tissueSelected')) { - onClickSetTissue(tis); - } else { - onClickClearTissue(tis); - } - } + function clearMapTissueElColor(el) { + // Change appearance of label in body map. + // NOTE: label may be consist of multiple text elements, so traverse children + // TODO: Try replacing with CSS + if (el === null) { + return; } - - function setMapTissueEl(el) { - // set label dark - if (el !== null) { - el.classList.add('tissueSelected'); - el.style.fill = "black"; + el.style.fill = COLOR_UNSELECTED; var count = el.childElementCount; for (var i = 0; i < count; i++) { - el.children[i].style.fill = "black"; - } + el.children[i].style.fill = COLOR_UNSELECTED; } } - function onClickSetTissue(tis) { - // mark selected in tissue table + function setTissue(tis) { + // Mark selected in tissue list and body map var $tis = $('#' + tis); - $tis.addClass('tissueSelected'); - var colorPatch = $tis.prev(".tissueColor"); - colorPatch.removeClass('tissueNotSelectedColor'); + $tis.addClass(CLASS_TISSUE_SELECTED); + + // Change appearance of color patch in tissue list + var colorPatch = $tis.prev('.tissueColor'); var tisColor = colorPatch.data('tissueColor'); colorPatch.css('background-color', tisColor); - var $checkbox = $('#' + tis + ' > input'); - $checkbox.attr("checked", true); - var el = _svgDoc.getElementById(tis + "_Text_Hi"); - setMapTissueEl(el); + colorPatch.removeClass('tissueNotSelectedColor'); // TODO: less obfuscated approach to colored border + + // Set hidden input checkbox, for cart + $('#' + tis + ' > input').attr('checked', true); + + // Change appearance of label in body map + var el = _svgDoc.getElementById(tis + TEXT_HI); + if (el !== null) { + el.classList.add(CLASS_TISSUE_SELECTED); + setMapTissueElColor(el); + } } - function onClickClearTissue(tis) { - // unselect in tissue table + function clearTissue(tis) { + // Unselect in tissue table and body map var $tis = $('#' + tis); - $tis.removeClass('tissueSelected'); - colorPatch = $tis.prev(".tissueColor"); + $tis.removeClass(CLASS_TISSUE_SELECTED); + + // Change appearance of color patch in tissue list + colorPatch = $tis.prev('.tissueColor'); colorPatch.addClass('tissueNotSelectedColor'); - var $checkbox = $('#' + tis + ' > input'); - $checkbox.attr("checked", false); - var el = _svgDoc.getElementById(tis + "_Text_Hi"); + + // Clear hidden input checkbox + $('#' + tis + ' > input').attr('checked', false); + + // Change appearance of label in body map + var el = _svgDoc.getElementById(tis + TEXT_HI); if (el !== null) { - el.classList.remove('tissueSelected'); - el.style.fill = "#737373"; - var count = el.childElementCount; - for (var i = 0; i < count; i++) { - el.children[i].style.fill = "#737373"; + el.classList.remove(CLASS_TISSUE_SELECTED); + clearMapTissueElColor(el); } } + + function toggleTissue(tis) { + // Select/unselect tissue in list and body map + var $tis = $('#' + tis); + $tis.toggleClass(CLASS_TISSUE_SELECTED); + if ($tis.hasClass(CLASS_TISSUE_SELECTED)) { + setTissue(tis); + } else { + clearTissue(tis); + } } function toggleHighlightTissue(tis) { - var i; - var isHovered = false; - var el = _htmlDoc.getElementById(tis); - var $tis = $("#" + tis); - if (el !== null) { - el.classList.toggle('tissueHovered'); - var colorPatch = $tis.prev(".tissueColor"); - colorPatch.toggleClass('tissueHoveredColor'); - if (el.classList.contains('tissueHovered')) { - isHovered = true; - } + // Highlight tissue in body map and tissue table + // TODO: simplify me + + // Highlight tissue label and color patch in tissue table + var $tis = $('#' + tis); + if ($tis !== null) { + $tis.toggleClass(CLASS_TISSUE_HOVERED); + var $colorPatch = $tis.prev('.tissueColor'); + $colorPatch.toggleClass('tissueHoveredColor'); } - // below can likely replace 3 lines after - //this.classList.toggle('tissueSelected'); - textEl = _svgDoc.getElementById(tis + '_Text_Hi'); + var isHovered = $tis.hasClass(CLASS_TISSUE_HOVERED) ? true : false; + + // Highlight tissue in body map by changing text appearance, visually moving organ to top + // and adding a white (or sometimes blue if white background) surround ('aura') + + // Highlight tissue label in body map + // TODO: Try jQuery here + textEl = _svgDoc.getElementById(tis + TEXT_HI); if (textEl === null) { return; } - textEl.classList.toggle('tissueHovered'); - var line = $("#" + tis + "_Lead_Hi", _svgRoot); - var lineEl = _svgDoc.getElementById(tis + '_Lead_Hi'); - var pic = $("#" + tis + "_Pic_Hi", _svgRoot); - var picEl = _svgDoc.getElementById(tis + '_Pic_Hi'); - var aura = $("#" + tis + "_Aura_Hi", _svgRoot); - var auraEl = _svgDoc.getElementById(tis + '_Aura_Hi'); + // TODO: unify with text styling below. Perhaps just add class to children will do it. + textEl.classList.toggle(CLASS_TISSUE_HOVERED); + + var lineEl = _svgDoc.getElementById(tis + LEAD_HI); + var pic = $('#' + tis + PIC_HI, _svgRoot); + var picEl = _svgDoc.getElementById(tis + PIC_HI); + var aura = $('#' + tis + AURA_HI, _svgRoot); + var auraEl = _svgDoc.getElementById(tis + AURA_HI); var textLineCount = textEl.childElementCount; + var i; + if (isHovered) { - textEl.style.fill = 'blue'; + textEl.style.fill = COLOR_HIGHLIGHT; for (i = 0; i < textLineCount; i++) { - textEl.children[i].style.fill = "blue"; + textEl.children[i].style.fill = COLOR_HIGHLIGHT; } if (lineEl !== null) { // cell types lack leader lines - lineEl.style.stroke = 'blue'; + lineEl.style.stroke = COLOR_HIGHLIGHT; } $(pic).show(); $(aura).show(); var topAura = auraEl.cloneNode(true); - topAura.id = "topAura"; + topAura.id = 'topAura'; _topAura = _svgRoot.appendChild(topAura); var topTissue = picEl.cloneNode(true); - topTissue.id = "topTissue"; + topTissue.id = 'topTissue'; + topTissue.classList.add(tis); _topTissue = _svgRoot.appendChild(topTissue); } else { - var color = textEl.classList.contains('tissueSelected') ? 'black' : '#737373'; + var color = textEl.classList.contains(CLASS_TISSUE_SELECTED) ? COLOR_SELECTED : COLOR_UNSELECTED; textEl.style.fill = color; for (i = 0; i < textLineCount; i++) { textEl.children[i].style.fill = color; } $(aura).hide(); $(pic).hide(); if (lineEl !== null) { // cell types lack leader lines - lineEl.style.stroke = '#F69296'; // pink + lineEl.style.stroke = COLOR_LEADER; // pink } _svgRoot.removeChild(_topTissue); _svgRoot.removeChild(_topAura); } } - function onMapHoverTissue(ev) { - var svgId = ev.target.id; + // Event handlers + + function onClickSetAll() { + // Set all on body map and tissue list + tissues.forEach(setTissue); + } + + function onClickClearAll() { + // Clear all on body map and tissue list + tissues.forEach(clearTissue); + } + + function onClickToggleTissue(tis) { + // Select/unselect from tissue list + tis = this.id; // arg bad + toggleTissue(tis); + } + + function onMapClickToggleTissue(ev) { + // Select/unselect from illustration + var svgId = ev.currentTarget.id; var tis = tissueFromSvgId(svgId); - toggleHighlightTissue(tis); + toggleTissue(tis); } function onHoverTissue() { - var tis = this.id; + // Mouseover on label in tissue list + toggleHighlightTissue(this.id); + } + + function onMapHoverTissue(ev) { + // Mouseover on tissue shape or label in illustration + var svgId = ev.currentTarget.id; + var tis = tissueFromSvgId(svgId); toggleHighlightTissue(tis); } + function submitForm() { + // Submit the form (from GO button -- as in hgGateway.js) + // Show a spinner -- sometimes it takes a while for hgTracks to start displaying. + $('.gbGoIcon').removeClass('fa-play').addClass('fa-spinner fa-spin'); + $form = $('form'); + $form.submit(); + } + + // Initialization + + function initTissue(tis) { + // Set tissue to unhighlighted state + $('#' + tis + PIC_HI, _svgRoot).hide(); + $('#' + tis + AURA_HI, _svgRoot).hide(); + + // Mark tissue labels in svg + var textEl = _svgDoc.getElementById(tis + TEXT_HI); + if (textEl !== null) { + textEl.classList.add('tissueLabel'); + if ($('#' + tis).hasClass(CLASS_TISSUE_SELECTED)) { + textEl.classList.add(CLASS_TISSUE_SELECTED); + setMapTissueElColor(textEl); + } + } + } + + function initBodyMap() { + // Set organs to unhighlighted state + tissues.forEach(initTissue); + } + function animateTissue(tis) { - // Add handlers to tissue table - var textEl; - var picEl; + // Add event handlers to body map and tissue list + + // Add click and mouseover handler to tissue label in tissue list $('#' + tis).click(tis, onClickToggleTissue); $('#' + tis).hover(onHoverTissue, onHoverTissue); - // Add mouseover and click handlers to tissue label - textEl = _svgDoc.getElementById(tis + "_Text_Hi"); + // Add mouseover and click handlers to tissue label in body map + var textEl = _svgDoc.getElementById(tis + TEXT_HI); if (textEl !== null) { - if ($('#' + tis).hasClass('tissueSelected')) { - setMapTissueEl(textEl); - } - textEl.addEventListener("click", onMapClickToggleTissue); - textEl.addEventListener("mouseenter", onMapHoverTissue); - textEl.addEventListener("mouseleave", onMapHoverTissue); - // mouseover, mouseout ? + textEl.addEventListener('click', onMapClickToggleTissue); + textEl.addEventListener('mouseenter', onMapHoverTissue); + textEl.addEventListener('mouseleave', onMapHoverTissue); } // add mouseover and click handlers to tissue shape - picEl = _svgDoc.getElementById(tis + "_Pic_Lo"); + var picEl = _svgDoc.getElementById(tis + PIC_LO); if (picEl !== null) { - picEl.addEventListener("click", onMapClickToggleTissue); - picEl.addEventListener("mouseenter", onMapHoverTissue); - picEl.addEventListener("mouseleave", onMapHoverTissue); - // mouseover, mouseout ? + picEl.addEventListener('mouseenter', onMapHoverTissue); + picEl.addEventListener('mouseleave', onMapHoverTissue); + picEl.addEventListener('mouseup', onMapClickToggleTissue); } } function animateTissues() { // Add event handlers to tissue table and body map SVG - tissues.forEach(animateTissue); - $('#setAll').click(onClickSetAll); $('#clearAll').click(onClickClearAll); + tissues.forEach(animateTissue); } - // UI event handlers - - function onClickSetAll() { - // Set all on body map - tissues.forEach(onClickSetTissue); - // set all on tissue table - // NOTE: this shouldn't be needed (needs debugging in onClickSet) - $('.tissueLabel').addClass('tissueSelected'); - } - - function onClickClearAll() { - tissues.forEach(onClickClearTissue); - $('.tissueLabel').removeClass("tissueSelected"); - } - - function submitForm() { - // Submit the form (from GO button -- as in hgGateway.js) - // Show a spinner -- sometimes it takes a while for hgTracks to start displaying. - $('.gbGoIcon').removeClass('fa-play').addClass('fa-spinner fa-spin'); - $form = $('form'); - $form.submit(); - } - - // Initialization - function init() { // cart.setCgi('gtexTrackSettings'); $(function() { // After page load, tweak layout and initialize event handlers // TODO: need to wait onready ? - var bodyMapSvg = document.getElementById("bodyMapSvg"); - globalSvg = bodyMapSvg; - - _htmlDoc = document; - _htmlRoot = document.documentElement; + var bodyMapSvg = document.getElementById('bodyMapSvg'); // Wait for SVG to load - bodyMapSvg.addEventListener("load", function() { - var svg = bodyMapSvg.contentDocument.documentElement; - _svgRoot = svg; + bodyMapSvg.addEventListener('load', function() { + _svgRoot = bodyMapSvg.contentDocument.documentElement; _svgDoc = bodyMapSvg.contentDocument; - initBodyMap(svg, bodyMapSvg.contentDocument); + initBodyMap(); animateTissues(); }, false); $('.goButtonContainer').click(submitForm); }); } return { init: init }; }()); // gtexTrackSettings -$(document).ready(function() { gtexTrackSettings.init(); -}); - -//gtexTrackSettings.init();