4bb9e8caea515342ba98d3871da76cd4ec69916f chmalee Fri May 1 14:10:00 2026 -0700 Initial myVariants implementation: a form on hgTracks where users can enter item details in one of three ways: hgvs/item search, simple bed form, advanced bed form where additional non-bed fields can dynamically created. Allows changing the color of items, writing descriptions, and editing the items after creation. Show overlaps with hardcoded tracks when hgc page is open (not in the hgc dialog). Next commit has implementation of sharing these tracks with other users diff --git src/hg/js/hgc.js src/hg/js/hgc.js index adc87e26351..4bdfab3a565 100644 --- src/hg/js/hgc.js +++ src/hg/js/hgc.js @@ -405,31 +405,131 @@ bar.setAttribute("height", heightPer); bar.setAttribute("fill", data.values[i].color); let barVal = document.createElementNS(svgNS, "text"); barVal.setAttribute("x", swatchColWidth + labelWidth + nWidth + thisBarWidth + padding); barVal.setAttribute("y", textY); barVal.setAttribute("font-size", fontSize); barVal.textContent = data.values[i].barValue; svg.appendChild(swatch); svg.appendChild(label); svg.appendChild(bar); svg.appendChild(barVal); } } +function createTrackOverlapRow(parentEle, trackName, trackLabel, data) { +/* Create a CSS grid row for trackLabel with the data in data, we will create + * enough columns to handle the number of fields in data, if there are multiple + * rows overlapping the item, then we will create subrows for the one track */ + const wrapper = document.createElement("div"); + wrapper.className = "section-row"; + + // Label on left + const title = document.createElement("div"); + title.className = "table-section-title"; + title.textContent = trackLabel; + wrapper.appendChild(title); + + const scrollWrapper = document.createElement("div"); + scrollWrapper.className = "table-scroll-wrapper"; + wrapper.appendChild(scrollWrapper); + + // Table grid on right + const grid = document.createElement("div"); + grid.className = "table-grid"; + scrollWrapper.appendChild(grid); + + const headers = Object.keys(data[trackName][0]); + const headerLen = headers.length; + + grid.style.gridTemplateColumns = `repeat(${headerLen}, minmax(120px,auto))`; + + // Header row + headers.forEach(h => { + const cell = document.createElement("div"); + cell.className = "table-cell table-header"; + cell.textContent = h; + grid.appendChild(cell); + }); + + // pad header row + for (let i = headers.length; i < headerLen; i++) { + grid.appendChild(document.createElement("div")).className = "table-cell table-header"; + } + + data[trackName].forEach( (rowData) => { + if (Object.keys(rowData).length !== headerLen) + { + alert(`Error: api output for ${trackName} has inconsistent number of fields`); + return; + } + + // data rows + Object.values(rowData).forEach(value => { + const cell = document.createElement("div"); + cell.className = "table-cell"; + cell.textContent = value; + grid.appendChild(cell); + }); + + // pad row + for (let i = Object.keys(rowData).length; i < headerLen; i++) { + grid.appendChild(document.createElement("div")).className = "table-cell"; + } + }); + parentEle.appendChild(wrapper); +} + +function fetchTableData(url) { + // return a Promise that will ultimately become JSON + return fetch(url).then(response => { + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return response.json(); + }); +} + function initPage() { + // Convert title attributes to proper mouseovers (for info icons etc.) + // This is needed for both standalone hgc and AJAX-loaded popup content + if (typeof convertTitleTagsToMouseovers === 'function') { + convertTitleTagsToMouseovers(); + } + + // Set up myVariants project dropdown handler + var projectSelects = document.querySelectorAll('[id$="_projectSelect"]'); + projectSelects.forEach(function(sel) { + sel.addEventListener('change', function() { + var inputId = this.id.replace('Select', ''); + var input = document.getElementById(inputId); + if (!input) { + console.error('myVariants project: Could not find input with id:', inputId); + return; + } + if (this.value === '__new__') { + input.style.display = ''; + input.value = ''; + input.focus(); + } else { + input.style.display = 'none'; + input.value = this.value; + } + }); + }); + if (typeof doHPRCTable !== "undefined") { makeHPRCTable(); } if (typeof svgTable !== "undefined") { // redraw the svg with appropriate widths for all columns // swatchWidth and columnSpacer are taken from svgBarChart() in hgc/barChartClick.c // they should probably be dynamically determined drawSvgTable(document.getElementById("svgBarChart"), barChartValues); } if (typeof _jsonHgcLabels !== "undefined") { let obj; for (obj of _jsonHgcLabels) { // build up the new table: var newTable = document.createElement("table"); var newRow = newTable.insertRow(); @@ -437,30 +537,48 @@ var label = obj.label; var data = obj.data; var newText = document.createTextNode(label); newCell.appendChild(newText); newCell = newRow.insertCell(); newCell.appendChild(dataToTable(label, data)); // find the last details table and add a new table on: var currTbl = $(".bedExtraTbl"); let l = currTbl.length; var last = currTbl[l-1]; insertAfter(newTable, last); newTable.classList.add("bedExtraTbl"); last.parentNode.insertBefore(document.createElement("br"), newTable); } } + + if (typeof doItemOverlaps !== 'undefined' && doItemOverlaps && typeof overlapTracks !== 'undefined' && overlapTracks.length > 0) { + // query hubApi looking for items that have the exact same coordinates as this item + // only look in tracks listed in overlapTracks array + // when we don't overlap, write a message saying so + let parentDiv = document.getElementById("itemOverlaps"); + parentDiv.style.display = "block"; + + const hubApiUrl = "../cgi-bin/hubApi/getData/track?genome="+getDb()+";chrom="+getURLParam("c")+";start="+getURLParam("o")+";end="+getURLParam("t")+";track="; + const requestUrls = overlapTracks.map(track => hubApiUrl+track); + const requests = requestUrls.map(req => fetchTableData(req)); + Promise.all(requests) + .then(results => { + results.forEach(data => createTrackOverlapRow(parentDiv, data.track, data.track, data)); + }).catch(err => console.error("Error fetching data:", err)); + + } + document.querySelectorAll('.hideToggle').forEach(function(element) { element.addEventListener('click', function() { var targetId = this.getAttribute('dataTarget'); var targetDiv = document.getElementById(targetId); var toggleImg = this.querySelector('img'); if (targetDiv.style.display === 'none') { targetDiv.style.display = 'block'; toggleImg.src = '../images/remove_sm.gif'; } else { targetDiv.style.display = 'none'; toggleImg.src = '../images/add_sm.gif'; } }); }); }