From 54bb7151ad3945d29dc5c1594f668ac5b8dd2974 Mon Sep 17 00:00:00 2001 From: Bradley Broom Date: Mon, 21 Jul 2025 08:26:09 -0500 Subject: [PATCH 01/11] Enhanced UserPreferenceManager.js to support multiple NG-CHMs When opened, the preferences manager will be for the current heat map. If the preferences manager is open when a new heatMap is selected, it will stay open for the old map and any updates will still apply to that map. If the user re-opens the preferences manager when it is already open, the open preferences manager will close and a new preferences manager for the current heat map will open. --- .../javascript/UserPreferenceManager.js | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/NGCHM/WebContent/javascript/UserPreferenceManager.js b/NGCHM/WebContent/javascript/UserPreferenceManager.js index ab1e031a..c53344a1 100644 --- a/NGCHM/WebContent/javascript/UserPreferenceManager.js +++ b/NGCHM/WebContent/javascript/UserPreferenceManager.js @@ -270,10 +270,19 @@ if (prefspanel.style.display !== "none") { // The user clicked to open the preferences panel, but we believe it is already open. - // Nothing to do, but we reshow the preferences panel just in case it's not visible. - prefspanel.style.display = ""; - locatePrefsPanel(); - return; + if (UPM.heatMap == heatMap) { + // Nothing to do, but we reshow the preferences panel just in case it's not visible. + prefspanel.style.display = ""; + locatePrefsPanel(); + return; + } else { + // Close preferences manager for old heatMap to allow for new one. + closePreferencesManager(); + // Let the display update before opening the new manager, so the + // user sees the preferences manager flash. + setTimeout (() => openPreferencesManager(heatMap), 100); + return; + } } // Set the heatMap we are editing. From fa848a59b7a3b31919456b6f54d3d2f598a0ac39 Mon Sep 17 00:00:00 2001 From: Bradley Broom Date: Mon, 21 Jul 2025 09:03:00 -0500 Subject: [PATCH 02/11] Enhanced PdfGenerator.js to support multiple NG-CHMs The generated PDF will only include the currently selected heat map. --- .../javascript/DetailHeatMapViews.js | 8 +++--- NGCHM/WebContent/javascript/PdfGenerator.js | 28 ++++++++----------- 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/NGCHM/WebContent/javascript/DetailHeatMapViews.js b/NGCHM/WebContent/javascript/DetailHeatMapViews.js index ffdfa0af..c7113a99 100644 --- a/NGCHM/WebContent/javascript/DetailHeatMapViews.js +++ b/NGCHM/WebContent/javascript/DetailHeatMapViews.js @@ -86,11 +86,11 @@ }; /********************************************************************************************* - * FUNCTION: anyVisible - Return true if any Detail View is visible. + * FUNCTION: anyVisible - Return true if any Detail View is visible for the specified heatMap. *********************************************************************************************/ - DVW.anyVisible = function anyVisible() { - for (let i = 0; i < DVW.detailMaps.length; i++) { - if (DVW.detailMaps[i].isVisible()) { + DVW.anyVisible = function anyVisible(heatMap) { + for (const mapItem of DVW.detailMaps) { + if (mapItem.heatMap == heatMap && mapItem.isVisible()) { return true; } } diff --git a/NGCHM/WebContent/javascript/PdfGenerator.js b/NGCHM/WebContent/javascript/PdfGenerator.js index 32b36c03..08e5f355 100644 --- a/NGCHM/WebContent/javascript/PdfGenerator.js +++ b/NGCHM/WebContent/javascript/PdfGenerator.js @@ -48,7 +48,7 @@ * button on the menu bar. The PDF preferences panel is then launched **********************************************************************************/ PDF.canGeneratePdf = function () { - return SUM.isVisible() || DVW.anyVisible(); + return SUM.isVisible() || DVW.anyVisible(MMGR.getHeatMap()); }; PDF.pdfDialogClosed = function () { @@ -64,7 +64,7 @@ let whyDisabled = "Cannot open the PDF dialog since it's already open"; if (prefspanel.classList.contains("hide")) { whyDisabled = - "Cannot generate a PDF when the Summary and all Detail heat map panels are closed."; + "Cannot generate a PDF when the Summary and all Detail heat map panels for the current heat map are closed."; } UHM.systemMessage("NG-CHM PDF Generator", whyDisabled); return; @@ -74,21 +74,15 @@ const sumButton = document.getElementById("pdfInputSummaryMap"); const detButton = document.getElementById("pdfInputDetailMap"); const bothButton = document.getElementById("pdfInputBothMaps"); - if (SUM.isVisible() && !DVW.anyVisible()) { + sumButton.disabled = !SUM.isVisible(); + detButton.disabled = !DVW.anyVisible(MMGR.getHeatMap()); + bothButton.disabled = sumButton.disabled || detButton.disabled; + if (!bothButton.disabled) { + bothButton.checked = true; + } else if (!sumButton.disabled) { sumButton.checked = true; - sumButton.disabled = false; - detButton.disabled = true; - bothButton.disabled = true; - } else if (DVW.anyVisible() && !SUM.isVisible()) { + } else if (!detButton.disabled) { detButton.checked = true; - sumButton.disabled = true; - detButton.disabled = false; - bothButton.disabled = true; - } else if (SUM.isVisible() && DVW.anyVisible()) { - bothButton.checked = true; - sumButton.disabled = false; - detButton.disabled = false; - bothButton.disabled = false; } else { // Should not happen. UHM.systemMessage( @@ -265,7 +259,7 @@ } if (includeDetailMaps) { DVW.detailMaps - .filter((mapItem) => mapItem.isVisible()) + .filter((mapItem) => mapItem.heatMap == heatMap && mapItem.isVisible()) .forEach((mapItem) => { drawJobs.push({ job: "detail", mapItem }); }); @@ -2125,7 +2119,7 @@ const yScale = sumMapH / heatMap.getNumRows(MAPREP.DETAIL_LEVEL); const xScale = sumMapW / heatMap.getNumColumns(MAPREP.DETAIL_LEVEL); DVW.detailMaps.forEach((mapItem) => { - if (mapItem.isVisible()) { + if (mapItem.heatMap == heatMap && mapItem.isVisible()) { const left = (mapItem.currentCol - 1) * xScale; const top = (mapItem.currentRow - 1) * yScale; const width = mapItem.dataPerRow * xScale; From e58b3a0e6df44545f0d6233ab1e0ce8d65db0075 Mon Sep 17 00:00:00 2001 From: Bradley Broom Date: Fri, 8 Aug 2025 12:27:44 -0500 Subject: [PATCH 03/11] Added heatmap-specific panel decorations During meeting discussions it was felt that adding heatmap-specific UI elements to the panel header would make it easier to know which panels applied to which heat maps. This commit implements that request. Remnants of a differently colored "active" pane header were also removed. These remnants were inactive because the CSS for the active pane header was commented out. Also, the determination of which panel was "active" was minimal and nowhere near complete. I removed these remnants to avoid potential confusion in the future. --- NGCHM/WebContent/css/NGCHM.css | 4 -- .../javascript/DetailHeatMapManager.js | 1 + NGCHM/WebContent/javascript/HeatMapClass.js | 2 + NGCHM/WebContent/javascript/Linkout.js | 1 + NGCHM/WebContent/javascript/MatrixManager.js | 9 +++ NGCHM/WebContent/javascript/NGCHM_Util.js | 54 +++++++++++++++ NGCHM/WebContent/javascript/PaneControl.js | 69 ++++++++++++++++++- .../javascript/SummaryHeatMapDisplay.js | 4 ++ .../javascript/SummaryHeatMapManager.js | 1 + 9 files changed, 139 insertions(+), 6 deletions(-) diff --git a/NGCHM/WebContent/css/NGCHM.css b/NGCHM/WebContent/css/NGCHM.css index f29263d1..7a3642c5 100644 --- a/NGCHM/WebContent/css/NGCHM.css +++ b/NGCHM/WebContent/css/NGCHM.css @@ -188,10 +188,6 @@ div.paneHeader { padding: 5px 5px 5px 5px; } -div.paneHeader.activePane { - /* background-color: #ccccff; */ -} - div.paneTitle { margin: 2px; display: inline-block; diff --git a/NGCHM/WebContent/javascript/DetailHeatMapManager.js b/NGCHM/WebContent/javascript/DetailHeatMapManager.js index f33e260a..7b69a0a7 100644 --- a/NGCHM/WebContent/javascript/DetailHeatMapManager.js +++ b/NGCHM/WebContent/javascript/DetailHeatMapManager.js @@ -613,6 +613,7 @@ isPrimary, restoreInfo ? restoreInfo.paneInfo : null, ); + PANE.setPaneDecor (loc, mapItem.heatMap.decor); // If primary is collapsed set chm detail of clone to visible if (!restoreInfo && chm.style.display === "none") { chm.style.display = ""; diff --git a/NGCHM/WebContent/javascript/HeatMapClass.js b/NGCHM/WebContent/javascript/HeatMapClass.js index 1e13570e..61dcbc2f 100644 --- a/NGCHM/WebContent/javascript/HeatMapClass.js +++ b/NGCHM/WebContent/javascript/HeatMapClass.js @@ -558,6 +558,8 @@ this.currentTileRequests = []; // Tiles we are currently reading this.pendingTileRequests = []; // Tiles we want to read this.searchOptions = []; // Search options specific to this heatMap. + this.searchState = new SRCHSTATE.SearchState(); // Search results for this heatMap. + this.decor = {}; // UI decorations for distinguishing one heatMap from another. this.updatedOnLoad = false; this.onready = onready; diff --git a/NGCHM/WebContent/javascript/Linkout.js b/NGCHM/WebContent/javascript/Linkout.js index 35d47b73..5966e91f 100644 --- a/NGCHM/WebContent/javascript/Linkout.js +++ b/NGCHM/WebContent/javascript/Linkout.js @@ -1575,6 +1575,7 @@ var linkoutsVersion = "undefined"; if (restoreInfo) { pluginRestoreInfo[loc.pane.id] = restoreInfo; } + PANE.setPaneDecor(loc, null); switchToPlugin(loc, plugin.name); MMGR.getHeatMap().setUnAppliedChanges(true); const params = plugin.params; diff --git a/NGCHM/WebContent/javascript/MatrixManager.js b/NGCHM/WebContent/javascript/MatrixManager.js index 4201362e..59cffb81 100644 --- a/NGCHM/WebContent/javascript/MatrixManager.js +++ b/NGCHM/WebContent/javascript/MatrixManager.js @@ -906,6 +906,15 @@ function saveMaps (callback) { return function onMapsLoaded (maps) { allHeatMaps = maps; + if (maps.length > 0) { + // Define map-specific decorations to help distinguish them in the UI. + for (let ii = 0; ii < maps.length; ii++) { + const map = maps[ii]; + // Spread out hues evenly as much as possible. + map.decor.hue = 360 * ii / maps.length; + map.decor.avatar = UTIL.genAvatar (map.decor.hue); + } + } MMGR.setHeatMap(allHeatMaps[0]); callback(); }; diff --git a/NGCHM/WebContent/javascript/NGCHM_Util.js b/NGCHM/WebContent/javascript/NGCHM_Util.js index 6cc645f7..e59f1986 100644 --- a/NGCHM/WebContent/javascript/NGCHM_Util.js +++ b/NGCHM/WebContent/javascript/NGCHM_Util.js @@ -162,6 +162,60 @@ return decorateElement(button, names, classes, attrs, [], fn); } + // Create an "avatar" of the specified hue. + // + UTIL.genAvatar = genAvatar; + function genAvatar (hue) { + const alines = [ "00100", "01010", "01110" ]; + const blines = [ "10001", "10101", "11011" ]; + const nums = crypto.randomUUID().split("").filter(x=>x!='-').map(x => parseInt(x,16)); + let d = ""; + const brow = nums[0] % 5; + for (let ii = 0; ii < 5; ii++) { + const chars = pickline(ii).split(""); + for (let jj = 0; jj < 5; jj++) { + if (chars[jj] == "1") { + d += B(jj,ii); + } + } + } + const sat = 100; + const val = 50 + Math.floor(Math.random() * 30); + const col = UTIL.hsvToRgb(hue,sat,val); + return ` + + `; + // Helper function. + function pickline (idx) { + let lines = blines.slice(); + if (idx != brow) { + lines = lines.concat(alines); + } + return lines[nums[idx+1] % lines.length]; + } + // Helper function. + // Return an SVG path element for a box at coordinates x, y. + function B(x,y) { + const scale = 3; + x *= scale; + y *= scale; + return `M${x} ${y} L${x+scale} ${y} L${x+scale} ${y+scale} L${x} ${y+scale}z`; + } + } + + // Create a button element containing the specified avatar. + // + UTIL.newAvatarButton = newAvatarButton; + function newAvatarButton(spec, avatar, attrs, fn) { + const classes = spec.split("."); + const names = classes.shift().split("#"); + const button = UTIL.newElement("BUTTON"); + button.innerHTML = avatar; + return decorateElement(button, names, classes, attrs, [], fn); + } + + // Create a button element containing the specified SVG icon(s). + // UTIL.newSvgMenuItem = newSvgMenuItem; function newSvgMenuItem(iconIds, attrs, fn) { const classes = iconIds.split("."); diff --git a/NGCHM/WebContent/javascript/PaneControl.js b/NGCHM/WebContent/javascript/PaneControl.js index 29f58c41..cc04c800 100644 --- a/NGCHM/WebContent/javascript/PaneControl.js +++ b/NGCHM/WebContent/javascript/PaneControl.js @@ -622,13 +622,16 @@ const pane = UTIL.newElement("DIV.pane", { style, id: paneid }); pane.addEventListener("paneresize", resizeHandler); if (title) { - const h = UTIL.newElement("DIV.paneHeader.activePane"); + const h = UTIL.newElement("DIV.paneHeader"); if (!PANE.showPaneHeader) h.classList.add("hide"); pane.appendChild(h); const sc = UTIL.newElement("DIV.paneScreenMode"); h.appendChild(sc); + const av = UTIL.newElement("DIV.paneAvatar"); + h.appendChild(av); + const t = UTIL.newElement("DIV.paneTitle"); t.innerText = title; h.appendChild(t); @@ -659,6 +662,67 @@ return pane; } + // Exported function. + // Set the heatMap-specific decorative UI elements. + PANE.setPaneDecor = setPaneDecor; + function setPaneDecor (loc, decor) { + // Remove previous custom decor. + loc.paneHeader.style.backgroundColor = ""; + const av = loc.paneHeader.getElementsByClassName("paneAvatar")[0]; + while (av.firstChild) av.firstChild.remove(); + if (!decor) { + // Quit if no custom decor. + return; + } + if (decor.hasOwnProperty('hue')) { + const bgColor = UTIL.hsvToRgb (decor.hue, 10, 90); + loc.paneHeader.style.backgroundColor = bgColor; + } + if (decor.avatar) { + const avatar = + UTIL.newAvatarButton( + ".ngchm-avatar", + decor.avatar, + { + dataset: { + tooltip: + "Avatar button", + title: "avatar-button", + intro: + "Long avatar button text", + }, + }, + (el) => { + el.onmousedown = (ev) => { + let button = ev.target; + while (button && button.tagName.toLowerCase() != "button") { + button = button.parentElement; + } + if (button) { + button.dataset.mouseDownTime = "" + performance.now(); + } + console.log ("Avatar mousedown", { button }); + }; + el.onclick = (ev) => { + ev.stopPropagation(); + let button = ev.target; + while (button && button.tagName.toLowerCase() != "button") { + button = button.parentElement; + } + if (button) { + const mapItem = findMapItem(ev); + console.log ("Avatar button click", { button, mapItem }); + } else { + console.log ("Avatar click"); + } + }; + return el; + }, + ); + av.appendChild(avatar); + } + } + // Exported function. // Add a group of icons to the pane header. // Spec is an object with the following entries: @@ -761,7 +825,6 @@ if (debug) console.log({ m: "splitPane", vertical, loc }); if (!loc.pane || !loc.container) return; const verticalContainer = loc.container.classList.contains("vertical"); - if (loc.paneHeader) loc.paneHeader.classList.remove("activePane"); const style = {}; style[vertical ? "height" : "width"] = "calc(50% - 5px)"; const divider = UTIL.newElement("DIV.resizerHelper"); @@ -1476,6 +1539,8 @@ setPaneTitle(loc, "empty"); removePanelMenuGroupIcons(loc); PANE.setPaneClientIcons(loc, {}); + // Clear any panel decoration. + PANE.setPaneDecor(loc, null); MMGR.getHeatMap().removePaneInfoFromMapConfig(loc.pane.id); // Return remaining client elements to caller. return clientElements; diff --git a/NGCHM/WebContent/javascript/SummaryHeatMapDisplay.js b/NGCHM/WebContent/javascript/SummaryHeatMapDisplay.js index 59d6a800..2182cfe9 100644 --- a/NGCHM/WebContent/javascript/SummaryHeatMapDisplay.js +++ b/NGCHM/WebContent/javascript/SummaryHeatMapDisplay.js @@ -92,6 +92,10 @@ SUM.colTopItemsWidth = 0; SUM.rowTopItemsHeight = 0; SUM.initSummaryData(); + if (SUM.chmElement) { + const loc = PANE.findPaneLocation(SUM.chmElement); + PANE.setPaneDecor (loc, SUM.heatMap.decor); + } }; // Callback that is notified every time there is an update to the heat map diff --git a/NGCHM/WebContent/javascript/SummaryHeatMapManager.js b/NGCHM/WebContent/javascript/SummaryHeatMapManager.js index ba89fae6..1b76df68 100644 --- a/NGCHM/WebContent/javascript/SummaryHeatMapManager.js +++ b/NGCHM/WebContent/javascript/SummaryHeatMapManager.js @@ -352,6 +352,7 @@ PANE.emptyPaneLocation(loc); initializeSummaryPanel(loc.pane); PANE.setPaneTitle(loc, "Heat Map Summary"); + PANE.setPaneDecor (loc, SUM.heatMap.decor); PANE.registerPaneEventHandler(loc.pane, "empty", emptySummaryPane); PANE.registerPaneEventHandler(loc.pane, "resize", resizeSummaryPane); loc.pane.dataset.title = "Summary Heat Map"; From 6a0cc1232d8b948c30cff45547647ea9bbf67df0 Mon Sep 17 00:00:00 2001 From: Bradley Broom Date: Tue, 26 Aug 2025 23:33:43 -0500 Subject: [PATCH 04/11] Small changes in response to CodeRabbit suggestions - Changed to using strict equality test everywhere I could find where heat maps were compared. - Did not add backward compatibility to anyVisible function, since we want the heatMap concerned to be specified explicitly. - Referenced the namespaced method UPM.openPreferencesManager. - I see no need to worry about debouncing this event at this time. - Added fallback for crypto.randomUUID. - Improved default button semantics and accessibility for avatar button. - Decided not to implement fallback for heatMap.decor since it is set in the constuctor and not unset. - Reordered call to PANE.setPaneDecor in DetailHeatMapManager.js. - Removed setting of heatMap.searchState to a subsequent pull request. - Removed call to findMapItem for now, but left other trace in place. --- NGCHM/WebContent/javascript/DetailHeatMapManager.js | 4 ++-- NGCHM/WebContent/javascript/DetailHeatMapViews.js | 2 +- NGCHM/WebContent/javascript/HeatMapClass.js | 1 - NGCHM/WebContent/javascript/HeatMapCommands.js | 2 +- NGCHM/WebContent/javascript/MatrixManager.js | 2 +- NGCHM/WebContent/javascript/NGCHM_Util.js | 6 +++++- NGCHM/WebContent/javascript/PaneControl.js | 4 ++-- NGCHM/WebContent/javascript/PdfGenerator.js | 4 ++-- NGCHM/WebContent/javascript/SearchManager.js | 2 +- NGCHM/WebContent/javascript/UserPreferenceManager.js | 4 ++-- 10 files changed, 17 insertions(+), 14 deletions(-) diff --git a/NGCHM/WebContent/javascript/DetailHeatMapManager.js b/NGCHM/WebContent/javascript/DetailHeatMapManager.js index 7b69a0a7..dba0bb7d 100644 --- a/NGCHM/WebContent/javascript/DetailHeatMapManager.js +++ b/NGCHM/WebContent/javascript/DetailHeatMapManager.js @@ -335,7 +335,7 @@ // Set a detail panel for heatMap as primary, if possible. DMM.setPrimaryForHeatmap = function setPrimaryForHeatmap (heatMap) { - const maps = DVW.detailMaps.filter(mapItem => mapItem.heatMap == heatMap); + const maps = DVW.detailMaps.filter(mapItem => mapItem.heatMap === heatMap); DMM.switchToPrimary(maps.length > 0 ? maps[0] : null); }; @@ -613,7 +613,6 @@ isPrimary, restoreInfo ? restoreInfo.paneInfo : null, ); - PANE.setPaneDecor (loc, mapItem.heatMap.decor); // If primary is collapsed set chm detail of clone to visible if (!restoreInfo && chm.style.display === "none") { chm.style.display = ""; @@ -623,6 +622,7 @@ document.getElementById("primary_btn" + mapNumber).dataset.version = isPrimary ? "P" : "S"; PANE.setPaneTitle(loc, "Heat Map Detail"); + PANE.setPaneDecor(loc, mapItem.heatMap.decor); PANE.registerPaneEventHandler(loc.pane, "empty", emptyDetailPane); PANE.registerPaneEventHandler(loc.pane, "resize", resizeDetailPane); DET.setDrawDetailTimeout(mapItem, 0, false); diff --git a/NGCHM/WebContent/javascript/DetailHeatMapViews.js b/NGCHM/WebContent/javascript/DetailHeatMapViews.js index c7113a99..fad3da8b 100644 --- a/NGCHM/WebContent/javascript/DetailHeatMapViews.js +++ b/NGCHM/WebContent/javascript/DetailHeatMapViews.js @@ -90,7 +90,7 @@ *********************************************************************************************/ DVW.anyVisible = function anyVisible(heatMap) { for (const mapItem of DVW.detailMaps) { - if (mapItem.heatMap == heatMap && mapItem.isVisible()) { + if (mapItem.heatMap === heatMap && mapItem.isVisible()) { return true; } } diff --git a/NGCHM/WebContent/javascript/HeatMapClass.js b/NGCHM/WebContent/javascript/HeatMapClass.js index 61dcbc2f..fd155289 100644 --- a/NGCHM/WebContent/javascript/HeatMapClass.js +++ b/NGCHM/WebContent/javascript/HeatMapClass.js @@ -558,7 +558,6 @@ this.currentTileRequests = []; // Tiles we are currently reading this.pendingTileRequests = []; // Tiles we want to read this.searchOptions = []; // Search options specific to this heatMap. - this.searchState = new SRCHSTATE.SearchState(); // Search results for this heatMap. this.decor = {}; // UI decorations for distinguishing one heatMap from another. this.updatedOnLoad = false; diff --git a/NGCHM/WebContent/javascript/HeatMapCommands.js b/NGCHM/WebContent/javascript/HeatMapCommands.js index a6e4477a..78f153cc 100644 --- a/NGCHM/WebContent/javascript/HeatMapCommands.js +++ b/NGCHM/WebContent/javascript/HeatMapCommands.js @@ -108,7 +108,7 @@ output.write(); for (const heatMap of MMGR.getAllHeatMaps()) { const info = heatMap.getMapInformation(); - const flag = heatMap == currentHeatmap ? "(*) " : ""; + const flag = heatMap === currentHeatmap ? "(*) " : ""; output.write (`${flag}${info.name}`); } output.unindent(); diff --git a/NGCHM/WebContent/javascript/MatrixManager.js b/NGCHM/WebContent/javascript/MatrixManager.js index 59cffb81..5e2cea3a 100644 --- a/NGCHM/WebContent/javascript/MatrixManager.js +++ b/NGCHM/WebContent/javascript/MatrixManager.js @@ -541,7 +541,7 @@ // Add the modified config data. promise = addTextContents( entry.filename, - JSON.stringify(heatMap == map ? (mapConf || heatMap.mapConfig) : map.mapConfig), + JSON.stringify(heatMap === map ? (mapConf || heatMap.mapConfig) : map.mapConfig), ); } else if (keyVal.indexOf("/mapData.json") >= 0) { // Add the potentially modified data. diff --git a/NGCHM/WebContent/javascript/NGCHM_Util.js b/NGCHM/WebContent/javascript/NGCHM_Util.js index e59f1986..74048605 100644 --- a/NGCHM/WebContent/javascript/NGCHM_Util.js +++ b/NGCHM/WebContent/javascript/NGCHM_Util.js @@ -168,7 +168,10 @@ function genAvatar (hue) { const alines = [ "00100", "01010", "01110" ]; const blines = [ "10001", "10101", "11011" ]; - const nums = crypto.randomUUID().split("").filter(x=>x!='-').map(x => parseInt(x,16)); + const guid = (window.crypto && typeof crypto.randomUUID === "function") + ? crypto.randomUUID() + : UTIL.getNewNonce(); + const nums = guid.split("").filter(x=>x!='-').map(x => parseInt(x,16)); let d = ""; const brow = nums[0] % 5; for (let ii = 0; ii < 5; ii++) { @@ -209,6 +212,7 @@ function newAvatarButton(spec, avatar, attrs, fn) { const classes = spec.split("."); const names = classes.shift().split("#"); + attrs = Object.assign({ type: "button", "aria-label": "Heat map avatar" }, attrs); const button = UTIL.newElement("BUTTON"); button.innerHTML = avatar; return decorateElement(button, names, classes, attrs, [], fn); diff --git a/NGCHM/WebContent/javascript/PaneControl.js b/NGCHM/WebContent/javascript/PaneControl.js index cc04c800..9297e8b6 100644 --- a/NGCHM/WebContent/javascript/PaneControl.js +++ b/NGCHM/WebContent/javascript/PaneControl.js @@ -710,8 +710,8 @@ button = button.parentElement; } if (button) { - const mapItem = findMapItem(ev); - console.log ("Avatar button click", { button, mapItem }); + const paneLoc = PANE.findPaneLocation(ev.target); + console.log ("Avatar button click", { button, paneLoc }); } else { console.log ("Avatar click"); } diff --git a/NGCHM/WebContent/javascript/PdfGenerator.js b/NGCHM/WebContent/javascript/PdfGenerator.js index 08e5f355..8d625387 100644 --- a/NGCHM/WebContent/javascript/PdfGenerator.js +++ b/NGCHM/WebContent/javascript/PdfGenerator.js @@ -259,7 +259,7 @@ } if (includeDetailMaps) { DVW.detailMaps - .filter((mapItem) => mapItem.heatMap == heatMap && mapItem.isVisible()) + .filter((mapItem) => mapItem.heatMap === heatMap && mapItem.isVisible()) .forEach((mapItem) => { drawJobs.push({ job: "detail", mapItem }); }); @@ -2119,7 +2119,7 @@ const yScale = sumMapH / heatMap.getNumRows(MAPREP.DETAIL_LEVEL); const xScale = sumMapW / heatMap.getNumColumns(MAPREP.DETAIL_LEVEL); DVW.detailMaps.forEach((mapItem) => { - if (mapItem.heatMap == heatMap && mapItem.isVisible()) { + if (mapItem.heatMap === heatMap && mapItem.isVisible()) { const left = (mapItem.currentCol - 1) * xScale; const top = (mapItem.currentRow - 1) * yScale; const width = mapItem.dataPerRow * xScale; diff --git a/NGCHM/WebContent/javascript/SearchManager.js b/NGCHM/WebContent/javascript/SearchManager.js index d2fb6ff3..f23d232b 100644 --- a/NGCHM/WebContent/javascript/SearchManager.js +++ b/NGCHM/WebContent/javascript/SearchManager.js @@ -536,7 +536,7 @@ const searchInterface = new SearchInterface(); SRCH.heatMapListener = function heatMapListener (heatMap, event) { - if (searchInterface.heatMap == heatMap && event == HEAT.Event_PLUGINS) { + if (searchInterface.heatMap === heatMap && event == HEAT.Event_PLUGINS) { SRCH.configSearchInterface (heatMap); } }; diff --git a/NGCHM/WebContent/javascript/UserPreferenceManager.js b/NGCHM/WebContent/javascript/UserPreferenceManager.js index c53344a1..d265316d 100644 --- a/NGCHM/WebContent/javascript/UserPreferenceManager.js +++ b/NGCHM/WebContent/javascript/UserPreferenceManager.js @@ -270,7 +270,7 @@ if (prefspanel.style.display !== "none") { // The user clicked to open the preferences panel, but we believe it is already open. - if (UPM.heatMap == heatMap) { + if (UPM.heatMap === heatMap) { // Nothing to do, but we reshow the preferences panel just in case it's not visible. prefspanel.style.display = ""; locatePrefsPanel(); @@ -280,7 +280,7 @@ closePreferencesManager(); // Let the display update before opening the new manager, so the // user sees the preferences manager flash. - setTimeout (() => openPreferencesManager(heatMap), 100); + setTimeout (() => UPM.openPreferencesManager(heatMap), 100); return; } } From 0f602a86a953f31d7c897262487ab16625fe2549 Mon Sep 17 00:00:00 2001 From: Bradley Broom Date: Tue, 26 Aug 2025 23:58:33 -0500 Subject: [PATCH 05/11] Enable automatic coderabbit reviews of pull requests to code-review branch --- .coderabbit.yaml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .coderabbit.yaml diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 00000000..40cd4404 --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,3 @@ +base_branches: + - main + - code-review From c99f8e31b218351311cdff25cf3e6ec358c0887c Mon Sep 17 00:00:00 2001 From: Bradley Broom Date: Wed, 27 Aug 2025 00:43:55 -0500 Subject: [PATCH 06/11] Fixes for issues identified by CodeRabbit. --- .coderabbit.yaml | 8 +++++--- NGCHM/WebContent/javascript/HeatMapClass.js | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 40cd4404..19ebe680 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -1,3 +1,5 @@ -base_branches: - - main - - code-review +reviews: + auto_review: + base_branches: + - main + - code-review diff --git a/NGCHM/WebContent/javascript/HeatMapClass.js b/NGCHM/WebContent/javascript/HeatMapClass.js index fd155289..3493694d 100644 --- a/NGCHM/WebContent/javascript/HeatMapClass.js +++ b/NGCHM/WebContent/javascript/HeatMapClass.js @@ -560,7 +560,7 @@ this.searchOptions = []; // Search options specific to this heatMap. this.decor = {}; // UI decorations for distinguishing one heatMap from another. - this.updatedOnLoad = false; + this.mapUpdatedOnLoad = false; this.onready = onready; this.initEventListeners(updateCallbacks); // Initialize this.eventListeners this.initTileWindows(); // Initialize this.tileWindowListeners and this.tileWindowRefs From 9f214b612f1f55560c04f4f97967e9fd30a5d608 Mon Sep 17 00:00:00 2001 From: Bradley Broom Date: Wed, 27 Aug 2025 07:59:55 -0500 Subject: [PATCH 07/11] Fixed several issues in HeatMapClass.js based on CodeRabbit comments --- NGCHM/WebContent/javascript/HeatMapClass.js | 52 ++++++++++----------- 1 file changed, 24 insertions(+), 28 deletions(-) diff --git a/NGCHM/WebContent/javascript/HeatMapClass.js b/NGCHM/WebContent/javascript/HeatMapClass.js index 3493694d..4cd5c866 100644 --- a/NGCHM/WebContent/javascript/HeatMapClass.js +++ b/NGCHM/WebContent/javascript/HeatMapClass.js @@ -72,7 +72,7 @@ this.endColTile = endColTile; this.numColumnTiles = endColTile - startColTile + 1; this.totalTiles = (endRowTile - startRowTile + 1) * this.numColumnTiles; - this.tiles = new Array({ length: this.totalTiles }); + this.tiles = new Array(this.totalTiles); // Get hard references to tile data if it's already in the TileCache. // Set _allTilesAvailable to false if any tile's data is not available. @@ -558,7 +558,7 @@ this.currentTileRequests = []; // Tiles we are currently reading this.pendingTileRequests = []; // Tiles we want to read this.searchOptions = []; // Search options specific to this heatMap. - this.decor = {}; // UI decorations for distinguishing one heatMap from another. + this.decor = { hue: null, avatar: null }; // UI decorations for distinguishing one heatMap from another. this.mapUpdatedOnLoad = false; this.onready = onready; @@ -834,11 +834,11 @@ } HeatMap.prototype.getRowClassificationOrder = function (showOnly) { - var rowClassBarsOrder = this.mapConfig.row_configuration.classifications_order; + let rowClassBarsOrder = this.mapConfig.row_configuration.classifications_order; // If configuration not found, create order from classifications config if (typeof rowClassBarsOrder === "undefined") { rowClassBarsOrder = []; - for (key in this.mapConfig.row_configuration.classifications) { + for (const key in this.mapConfig.row_configuration.classifications) { rowClassBarsOrder.push(key); } } @@ -847,9 +847,9 @@ return rowClassBarsOrder; } else { const filterRowClassBarsOrder = []; - for (var i = 0; i < rowClassBarsOrder.length; i++) { - var newKey = rowClassBarsOrder[i]; - var currConfig = this.mapConfig.row_configuration.classifications[newKey]; + for (let i = 0; i < rowClassBarsOrder.length; i++) { + const newKey = rowClassBarsOrder[i]; + const currConfig = this.mapConfig.row_configuration.classifications[newKey]; if (currConfig.show == "Y") { filterRowClassBarsOrder.push(newKey); } @@ -865,11 +865,11 @@ }; HeatMap.prototype.getColClassificationOrder = function (showOnly) { - var colClassBarsOrder = this.mapConfig.col_configuration.classifications_order; + let colClassBarsOrder = this.mapConfig.col_configuration.classifications_order; // If configuration not found, create order from classifications config if (typeof colClassBarsOrder === "undefined") { colClassBarsOrder = []; - for (key in this.mapConfig.col_configuration.classifications) { + for (const key in this.mapConfig.col_configuration.classifications) { colClassBarsOrder.push(key); } } @@ -878,9 +878,9 @@ return colClassBarsOrder; } else { const filterColClassBarsOrder = []; - for (var i = 0; i < colClassBarsOrder.length; i++) { - var newKey = colClassBarsOrder[i]; - var currConfig = this.mapConfig.col_configuration.classifications[newKey]; + for (let i = 0; i < colClassBarsOrder.length; i++) { + const newKey = colClassBarsOrder[i]; + const currConfig = this.mapConfig.col_configuration.classifications[newKey]; if (currConfig.show == "Y") { filterColClassBarsOrder.push(newKey); } @@ -1341,12 +1341,7 @@ /*********** Methods for accessing datalevels ****************/ - //Return the total number of detail rows - HeatMap.prototype.getTotalRows = function () { - return this.datalevels[MAPREP.DETAIL_LEVEL].totalRows; - }; - - //Return the summary row ratio + // Return the summary row ratio. HeatMap.prototype.getSummaryRowRatio = function () { if (this.datalevels[MAPREP.SUMMARY_LEVEL] !== null) { return this.datalevels[MAPREP.SUMMARY_LEVEL].rowSummaryRatio; @@ -1355,52 +1350,53 @@ } }; - //Return the summary row ratio + // Return the summary column ratio. HeatMap.prototype.getSummaryColRatio = function () { if (this.datalevels[MAPREP.SUMMARY_LEVEL] !== null) { return this.datalevels[MAPREP.SUMMARY_LEVEL].colSummaryRatio; } else { - return this.datalevels[MAPREP.THUMBNAIL_LEVEL].col_summaryRatio; + return this.datalevels[MAPREP.THUMBNAIL_LEVEL].colSummaryRatio; } }; - //Return the total number of detail rows + + // Return the total number of detail rows. HeatMap.prototype.getTotalRows = function () { return this.datalevels[MAPREP.DETAIL_LEVEL].totalRows; }; - //Return the total number of detail rows + // Return the total number of detail columns. HeatMap.prototype.getTotalCols = function () { return this.datalevels[MAPREP.DETAIL_LEVEL].totalColumns; }; - //Return the total number of rows/columns on the specified axis. + // Return the total number of rows/columns on the specified axis. HeatMap.prototype.getTotalElementsForAxis = function (axis) { const level = this.datalevels[MAPREP.DETAIL_LEVEL]; return MAPREP.isRow(axis) ? level.totalRows : level.totalColumns; }; - //Return the number of rows or columns for the given level + // Return the number of rows or columns for the given level HeatMap.prototype.getNumAxisElements = function (axis, level) { const l = this.datalevels[level]; return MAPREP.isRow(axis) ? l.totalRows : l.totalColumns; }; - //Return the number of rows for a given level + // Return the number of rows for a given level. HeatMap.prototype.getNumRows = function (level) { return this.datalevels[level].totalRows; }; - //Return the number of columns for a given level + // Return the number of columns for a given level. HeatMap.prototype.getNumColumns = function (level) { return this.datalevels[level].totalColumns; }; - //Return the row summary ratio for a given level + // Return the row summary ratio for a given level. HeatMap.prototype.getRowSummaryRatio = function (level) { return this.datalevels[level].rowSummaryRatio; }; - //Return the column summary ratio for a given level + // Return the column summary ratio for a given level. HeatMap.prototype.getColSummaryRatio = function (level) { return this.datalevels[level].colSummaryRatio; }; From 1bf31d2b1b3a0a9710064ac634f14510bd15f87f Mon Sep 17 00:00:00 2001 From: Bradley Broom Date: Wed, 27 Aug 2025 09:00:32 -0500 Subject: [PATCH 08/11] Fixed classification order issues Implemented suggestions from CodeRabbit: - Fixed check for undefined summary level - Used Object.keys instead of for..in iteration over objects Also, consolidated the near identical row and column functions for getting covariate orders into a single function with an axis parameter to eliminate duplicated code. Similarly, consolidated the functions for setting the initial covariate orders if not defined. --- .../javascript/CovariateCommands.js | 8 +- .../javascript/DetailHeatMapEvent.js | 4 +- NGCHM/WebContent/javascript/HeatMapClass.js | 81 +++++-------------- NGCHM/WebContent/javascript/Linkout.js | 4 +- NGCHM/WebContent/javascript/PdfGenerator.js | 8 +- NGCHM/WebContent/javascript/SearchManager.js | 2 +- .../javascript/SummaryHeatMapDisplay.js | 8 +- NGCHM/WebContent/javascript/UI-Manager.js | 4 +- 8 files changed, 41 insertions(+), 78 deletions(-) diff --git a/NGCHM/WebContent/javascript/CovariateCommands.js b/NGCHM/WebContent/javascript/CovariateCommands.js index 9eac9768..12a4a2c1 100644 --- a/NGCHM/WebContent/javascript/CovariateCommands.js +++ b/NGCHM/WebContent/javascript/CovariateCommands.js @@ -33,7 +33,7 @@ output.write(`${UTIL.toTitleCase(axis)} covariates:`); output.write(); output.indent(); - const order = req.heatMap.getAxisCovariateOrder(axis); + const order = req.heatMap.getCovariateOrder(axis); order.forEach((key) => output.write(key)); output.unindent(); output.write(); @@ -205,7 +205,7 @@ EXEC.noMoreParams, function (req, res, next) { // Check value appropriate for covariate data type. - const order = req.heatMap.getAxisCovariateOrder(req.axis); + const order = req.heatMap.getCovariateOrder(req.axis); if (order.includes(req.name)) { throw `${UTIL.toTitleCase(req.axis)} ${req.name} already exists`; } @@ -234,7 +234,7 @@ EXEC.reqCovariate, reqIndex, function (req, res) { - const order = req.heatMap.getAxisCovariateOrder(req.axis); + const order = req.heatMap.getCovariateOrder(req.axis); if (req.index > order.length) { throw `index must be between zero and the number of covariates, inclusive`; } @@ -275,7 +275,7 @@ EXEC.getHeatMap, EXEC.reqAxis, function (req, res) { - const order = req.heatMap.getAxisCovariateOrder(req.axis); + const order = req.heatMap.getCovariateOrder(req.axis); res.value = order.slice(); } ] diff --git a/NGCHM/WebContent/javascript/DetailHeatMapEvent.js b/NGCHM/WebContent/javascript/DetailHeatMapEvent.js index 339fef87..d8a1e3c6 100644 --- a/NGCHM/WebContent/javascript/DetailHeatMapEvent.js +++ b/NGCHM/WebContent/javascript/DetailHeatMapEvent.js @@ -263,7 +263,7 @@ const rowClassBars = heatMap.getRowClassificationData(); const rowClassConfig = heatMap.getRowClassificationConfig(); pixelInfo.rowCovariates = heatMap - .getRowClassificationOrder() + .getCovariateOrder("row") .filter((key) => rowClassConfig[key].show === "Y") .map((key) => ({ name: key, @@ -272,7 +272,7 @@ const colClassBars = heatMap.getColClassificationData(); const colClassConfig = heatMap.getColClassificationConfig(); pixelInfo.colCovariates = heatMap - .getColClassificationOrder() + .getCovariateOrder("column") .filter((key) => colClassConfig[key].show === "Y") .map((key) => ({ name: key, diff --git a/NGCHM/WebContent/javascript/HeatMapClass.js b/NGCHM/WebContent/javascript/HeatMapClass.js index 4cd5c866..d11b9af1 100644 --- a/NGCHM/WebContent/javascript/HeatMapClass.js +++ b/NGCHM/WebContent/javascript/HeatMapClass.js @@ -754,12 +754,6 @@ } }; - HeatMap.prototype.getAxisCovariateOrder = function (axis) { - return MAPREP.isRow(axis) - ? this.getRowClassificationOrder() - : this.getColClassificationOrder(); - }; - HeatMap.prototype.getRowClassificationConfig = function () { return this.mapConfig.row_configuration.classifications; }; @@ -804,7 +798,7 @@ // HeatMap.prototype.genAllCovars = function* genAllCovars() { for (const axis of ["row", "col"]) { - const covariates = this.getAxisCovariateOrder(axis); + const covariates = this.getCovariateOrder(axis); for (const key of covariates) { yield { axis, key }; } @@ -833,60 +827,35 @@ } } - HeatMap.prototype.getRowClassificationOrder = function (showOnly) { - let rowClassBarsOrder = this.mapConfig.row_configuration.classifications_order; - // If configuration not found, create order from classifications config - if (typeof rowClassBarsOrder === "undefined") { - rowClassBarsOrder = []; - for (const key in this.mapConfig.row_configuration.classifications) { - rowClassBarsOrder.push(key); - } + // Return the order in which the covariates on the specified axis should be + // displayed. + // If showOnly is specified, return only the visible covariates. + HeatMap.prototype.getCovariateOrder = function (axis, showOnly) { + const axisConfig = this.getAxisConfig(axis); + let barOrder = axisConfig.classifications_order; + // If barOrder not defined, create order from classifications config. + if (typeof barOrder === "undefined") { + barOrder = Object.keys(axisConfig.classifications); } // Filter order for ONLY shown bars (if requested) if (typeof showOnly === "undefined") { - return rowClassBarsOrder; + return barOrder; } else { - const filterRowClassBarsOrder = []; - for (let i = 0; i < rowClassBarsOrder.length; i++) { - const newKey = rowClassBarsOrder[i]; - const currConfig = this.mapConfig.row_configuration.classifications[newKey]; - if (currConfig.show == "Y") { - filterRowClassBarsOrder.push(newKey); - } - } - return filterRowClassBarsOrder; + return barOrder.filter( + (key) => axisConfig.classifications[key].show == "Y" + ); } }; - HeatMap.prototype.setRowClassificationOrder = function () { + // If not already defined, set the order in which the covariates on the + // specified axis should be displayed. + HeatMap.prototype.setCovariateOrder = function (axis) { if (this.mapConfig !== null) { - this.mapConfig.row_configuration.classifications_order = this.getRowClassificationOrder(); - } - }; - - HeatMap.prototype.getColClassificationOrder = function (showOnly) { - let colClassBarsOrder = this.mapConfig.col_configuration.classifications_order; - // If configuration not found, create order from classifications config - if (typeof colClassBarsOrder === "undefined") { - colClassBarsOrder = []; - for (const key in this.mapConfig.col_configuration.classifications) { - colClassBarsOrder.push(key); + const axisConfig = this.getAxisConfig(axis); + if (typeof axisConfig.classifications_order === "undefined") { + axisConfig.classifications_order = Object.keys(axisConfig.classifications); } } - // Filter order for ONLY shown bars (if requested) - if (typeof showOnly === "undefined") { - return colClassBarsOrder; - } else { - const filterColClassBarsOrder = []; - for (let i = 0; i < colClassBarsOrder.length; i++) { - const newKey = colClassBarsOrder[i]; - const currConfig = this.mapConfig.col_configuration.classifications[newKey]; - if (currConfig.show == "Y") { - filterColClassBarsOrder.push(newKey); - } - } - return filterColClassBarsOrder; - } }; const transparent = { r: 0, g: 0, b: 0, a: 0 }; @@ -966,12 +935,6 @@ ); }; - HeatMap.prototype.setColClassificationOrder = function () { - if (this.mapConfig !== null) { - this.mapConfig.col_configuration.classifications_order = this.getColClassificationOrder(); - } - }; - // Persist the axis covariate order and mark the heatmap as changed. HeatMap.prototype.setAxisCovariateOrder = function (axis, order) { if (!Array.isArray(order)) { @@ -1343,7 +1306,7 @@ // Return the summary row ratio. HeatMap.prototype.getSummaryRowRatio = function () { - if (this.datalevels[MAPREP.SUMMARY_LEVEL] !== null) { + if (this.datalevels[MAPREP.SUMMARY_LEVEL]) { return this.datalevels[MAPREP.SUMMARY_LEVEL].rowSummaryRatio; } else { return this.datalevels[MAPREP.THUMBNAIL_LEVEL].rowSummaryRatio; @@ -1352,7 +1315,7 @@ // Return the summary column ratio. HeatMap.prototype.getSummaryColRatio = function () { - if (this.datalevels[MAPREP.SUMMARY_LEVEL] !== null) { + if (this.datalevels[MAPREP.SUMMARY_LEVEL]) { return this.datalevels[MAPREP.SUMMARY_LEVEL].colSummaryRatio; } else { return this.datalevels[MAPREP.THUMBNAIL_LEVEL].colSummaryRatio; diff --git a/NGCHM/WebContent/javascript/Linkout.js b/NGCHM/WebContent/javascript/Linkout.js index 5966e91f..9d477a4a 100644 --- a/NGCHM/WebContent/javascript/Linkout.js +++ b/NGCHM/WebContent/javascript/Linkout.js @@ -446,7 +446,7 @@ var linkoutsVersion = "undefined"; } else { const srchResults = SRCHSTATE.getAxisSearchResults(axis); if (axis.includes("Covar")) { - const labels = heatMap.getAxisCovariateOrder(axis.replace("Covar","")); + const labels = heatMap.getCovariateOrder(axis.replace("Covar","")); return srchResults.map(idx => generateLinkoutLabel(labels[idx], formatIndex)); } else { const labels = heatMap.getAxisLabels(axis).labels; @@ -2521,7 +2521,7 @@ var linkoutsVersion = "undefined"; thisAxis = axis; if (debug) console.log({ m: "setAxis", axis, params }); axis1Config = heatMap.getAxisCovariateConfig(axis); - const axis1cvOrder = heatMap.getAxisCovariateOrder(axis); + const axis1cvOrder = heatMap.getCovariateOrder(axis); otherAxis = MAPREP.isRow(axis) ? "Column" : "Row"; if (plugin.hasOwnProperty("specialCoordinates") && plugin.specialCoordinates.hasOwnProperty("name")){ defaultCoord = plugin.specialCoordinates.name + ".coordinate."; diff --git a/NGCHM/WebContent/javascript/PdfGenerator.js b/NGCHM/WebContent/javascript/PdfGenerator.js index 8d625387..6e27c491 100644 --- a/NGCHM/WebContent/javascript/PdfGenerator.js +++ b/NGCHM/WebContent/javascript/PdfGenerator.js @@ -746,10 +746,10 @@ barsInfo.rowBarsToDraw = []; barsInfo.colBarsToDraw = []; if (isChecked("pdfInputColumn")) { - barsInfo.colBarsToDraw = heatMap.getColClassificationOrder("show"); + barsInfo.colBarsToDraw = heatMap.getCovariateOrder("column", "show"); } if (isChecked("pdfInputRow")) { - barsInfo.rowBarsToDraw = heatMap.getRowClassificationOrder("show"); + barsInfo.rowBarsToDraw = heatMap.getCovariateOrder("row", "show"); } barsInfo.topOff = pdfDoc.paddingTop + barsInfo.classBarTitleSize + 5; barsInfo.leftOff = 20; @@ -1818,13 +1818,13 @@ function isLastClassBarToBeDrawn(heatMap, classBar, type) { var isItLast = false; if (isChecked("pdfInputColumn")) { - var colBars = heatMap.getColClassificationOrder("show"); + var colBars = heatMap.getCovariateOrder("column", "show"); if (type === "col" && classBar === colBars[colBars.length - 1]) { isItLast = true; } } if (isChecked("pdfInputRow")) { - var rowBars = heatMap.getRowClassificationOrder("show"); + var rowBars = heatMap.getCovariateOrder("row", "show"); if (type === "row" && classBar === rowBars[rowBars.length - 1]) { isItLast = true; } diff --git a/NGCHM/WebContent/javascript/SearchManager.js b/NGCHM/WebContent/javascript/SearchManager.js index f23d232b..795a00bc 100644 --- a/NGCHM/WebContent/javascript/SearchManager.js +++ b/NGCHM/WebContent/javascript/SearchManager.js @@ -562,7 +562,7 @@ // Helper function. // Add a search option for every covariate on the specified axis. function addCovariateOptions(axis) { - for (const key of heatMap.getAxisCovariateOrder(axis)) { + for (const key of heatMap.getCovariateOrder(axis)) { searchInterface.addSearchOption({ type: heatMap.getCovariateType(axis, key), axis, diff --git a/NGCHM/WebContent/javascript/SummaryHeatMapDisplay.js b/NGCHM/WebContent/javascript/SummaryHeatMapDisplay.js index 2182cfe9..b8994615 100644 --- a/NGCHM/WebContent/javascript/SummaryHeatMapDisplay.js +++ b/NGCHM/WebContent/javascript/SummaryHeatMapDisplay.js @@ -1167,7 +1167,7 @@ var rowCanvas = document.getElementById("summary_row_top_items_canvas"); var sumCanvas = document.getElementById("summary_canvas"); var classBarsConfig = heatMap.getRowClassificationConfig(); - var classBarConfigOrder = heatMap.getRowClassificationOrder(); + var classBarConfigOrder = heatMap.getCovariateOrder("row"); var totalHeight = 0; var matrixWidth = colCanvas.width; //Calc total width of all covariate bars @@ -1218,7 +1218,7 @@ const heatMap = SUM.heatMap; SUM.removeColClassBarLabels(); var classBarsConfig = heatMap.getColClassificationConfig(); - var classBarConfigOrder = heatMap.getColClassificationOrder(); + var classBarConfigOrder = heatMap.getCovariateOrder("column"); var classBarsData = heatMap.getColClassificationData(); var prevHeight = 0; for (var i = 0; i < classBarConfigOrder.length; i++) { @@ -1278,7 +1278,7 @@ SUM.drawColClassBarLegends = function () { const heatMap = SUM.heatMap; var classBarsConfig = heatMap.getColClassificationConfig(); - var classBarConfigOrder = heatMap.getColClassificationOrder(); + var classBarConfigOrder = heatMap.getCovariateOrder("column"); var classBarsData = heatMap.getColClassificationData(); var totalHeight = 0; for (var i = 0; i < classBarConfigOrder.length; i++) { @@ -1473,7 +1473,7 @@ SUM.drawRowClassBarLegends = function () { const heatMap = SUM.heatMap; var classBarsConfig = heatMap.getRowClassificationConfig(); - var classBarConfigOrder = heatMap.getRowClassificationOrder(); + var classBarConfigOrder = heatMap.getCovariateOrder("row"); var classBarsData = heatMap.getRowClassificationData(); var totalHeight = 0; for (var i = 0; i < classBarConfigOrder.length; i++) { diff --git a/NGCHM/WebContent/javascript/UI-Manager.js b/NGCHM/WebContent/javascript/UI-Manager.js index 86f2c987..42fb37ea 100644 --- a/NGCHM/WebContent/javascript/UI-Manager.js +++ b/NGCHM/WebContent/javascript/UI-Manager.js @@ -84,8 +84,8 @@ function autoSaveHeatMap(heatMap) { let success = true; if (!srcInfo.embedded) { - heatMap.setRowClassificationOrder(); - heatMap.setColClassificationOrder(); + heatMap.setCovariateOrder("row"); + heatMap.setCovariateOrder("column"); switch (MMGR.getSource()) { case MMGR.API_SOURCE: // FIXME: BMB. Verify this does what it's required to do. From 4f2ef5c011061b3fdf23c5d90d5d29c2c2ab4008 Mon Sep 17 00:00:00 2001 From: Bradley Broom Date: Wed, 27 Aug 2025 10:00:25 -0500 Subject: [PATCH 09/11] Made further changes in response to CodeRabbit commits setAxisCovariateOrder was a separate function from setCovariateOrder (and the setRowClassificationOrder and setColClassificationOrder functions it replaced) and so there was no actual brokage. But, the two functions are very similar in both name and function and undoubtedly this would have led to confusion and errors in the future. I consolidated both functions into a single function that takes an optional order parameter. The jsPDF setDrawColor was working in my tests, but I followed CodeRabbit's suggestion to ensure compatibility. I made the suggested changes to improve the robustness of getCovariateOrder. I left fixing the redundancy in the totals and accessors API surface for another day. This is a rabbit hole I don't want to go down just now. --- .../javascript/CovariateCommands.js | 2 +- NGCHM/WebContent/javascript/HeatMapClass.js | 46 +++++++++---------- NGCHM/WebContent/javascript/PdfGenerator.js | 4 +- 3 files changed, 24 insertions(+), 28 deletions(-) diff --git a/NGCHM/WebContent/javascript/CovariateCommands.js b/NGCHM/WebContent/javascript/CovariateCommands.js index 12a4a2c1..6602c938 100644 --- a/NGCHM/WebContent/javascript/CovariateCommands.js +++ b/NGCHM/WebContent/javascript/CovariateCommands.js @@ -253,7 +253,7 @@ order.splice(oldIndex, 1); const insertIndex = Math.min(req.index, order.length); order.splice(insertIndex, 0, req.covariateName); - req.heatMap.setAxisCovariateOrder(req.axis, order); + req.heatMap.setCovariateOrder(req.axis, order); res.output.write(`Reordered ${req.axis} covariates`); } ] diff --git a/NGCHM/WebContent/javascript/HeatMapClass.js b/NGCHM/WebContent/javascript/HeatMapClass.js index d11b9af1..468fcd0b 100644 --- a/NGCHM/WebContent/javascript/HeatMapClass.js +++ b/NGCHM/WebContent/javascript/HeatMapClass.js @@ -832,26 +832,32 @@ // If showOnly is specified, return only the visible covariates. HeatMap.prototype.getCovariateOrder = function (axis, showOnly) { const axisConfig = this.getAxisConfig(axis); - let barOrder = axisConfig.classifications_order; // If barOrder not defined, create order from classifications config. - if (typeof barOrder === "undefined") { - barOrder = Object.keys(axisConfig.classifications); - } - // Filter order for ONLY shown bars (if requested) - if (typeof showOnly === "undefined") { - return barOrder; - } else { - return barOrder.filter( - (key) => axisConfig.classifications[key].show == "Y" - ); - } + const barOrder = Array.isArray(axisConfig.classifications_order) + ? axisConfig.classifications_order.slice() + : Object.keys(axisConfig.classifications); + // Filter order for only shown bars (if requested) + return showOnly === true || showOnly === "show" + ? barOrder.filter((key) => axisConfig.classifications[key].show == "Y") + : barOrder; }; - // If not already defined, set the order in which the covariates on the - // specified axis should be displayed. - HeatMap.prototype.setCovariateOrder = function (axis) { + // Set the order in which the covariates on the specified axis should be + // displayed. + // If order is not specified and there is no existing order, set a + // default order. + HeatMap.prototype.setCovariateOrder = function (axis, order) { if (this.mapConfig !== null) { const axisConfig = this.getAxisConfig(axis); + if (order) { + if (!Array.isArray(order)) { + console.error("setCovariateOrder: if specified, order must be an array", { axis, order }); + return; + } + axisConfig.classifications_order = order.slice(); + this.setUnAppliedChanges(true); + return; + } if (typeof axisConfig.classifications_order === "undefined") { axisConfig.classifications_order = Object.keys(axisConfig.classifications); } @@ -935,16 +941,6 @@ ); }; - // Persist the axis covariate order and mark the heatmap as changed. - HeatMap.prototype.setAxisCovariateOrder = function (axis, order) { - if (!Array.isArray(order)) { - console.error("setAxisCovariateOrder requires an order array", { axis, order }); - return; - } - this.getAxisConfig(axis).classifications_order = order.slice(); - this.setUnAppliedChanges(true); - }; - HeatMap.prototype.getMapInformation = function () { return this.mapConfig.data_configuration.map_information; }; diff --git a/NGCHM/WebContent/javascript/PdfGenerator.js b/NGCHM/WebContent/javascript/PdfGenerator.js index 6e27c491..88bd21f0 100644 --- a/NGCHM/WebContent/javascript/PdfGenerator.js +++ b/NGCHM/WebContent/javascript/PdfGenerator.js @@ -2114,8 +2114,8 @@ // Draw the 'green' boundary rectangles that outline the detail map views. if (showDetailViewBounds) { - const color = heatMap.getCurrentDataLayer().selection_color; - pdfDoc.doc.setDrawColor(color); + const sel = CMM.hexToRgba(heatMap.getCurrentDataLayer().selection_color); + pdfDoc.doc.setDrawColor(sel.r, sel.g, sel.b); const yScale = sumMapH / heatMap.getNumRows(MAPREP.DETAIL_LEVEL); const xScale = sumMapW / heatMap.getNumColumns(MAPREP.DETAIL_LEVEL); DVW.detailMaps.forEach((mapItem) => { From 05b0f4ff4be5d3f71ab0cb0562445feac2d4959a Mon Sep 17 00:00:00 2001 From: Bradley Broom Date: Wed, 27 Aug 2025 11:30:58 -0500 Subject: [PATCH 10/11] Made a couple of minor changes in response to CodeRabbit - Added default CodeRabbit review to drafr pull request on specified branches - Fixed error in isMatrixLinkout --- .coderabbit.yaml | 1 + NGCHM/WebContent/javascript/Linkout.js | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 19ebe680..08e3f018 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -3,3 +3,4 @@ reviews: base_branches: - main - code-review + drafts: true diff --git a/NGCHM/WebContent/javascript/Linkout.js b/NGCHM/WebContent/javascript/Linkout.js index 9d477a4a..eb35a5d2 100644 --- a/NGCHM/WebContent/javascript/Linkout.js +++ b/NGCHM/WebContent/javascript/Linkout.js @@ -408,9 +408,9 @@ var linkoutsVersion = "undefined"; } }; - // Returns TRUE iff there is a matrix linkout with the specified name. - LNK.isMatrixLinkout = function isMatrixLinkout (name) { - return matrixLinkouts.map(linkout => linkout.name).includes(name); + // Returns TRUE iff there is a matrix linkout with the specified title. + LNK.isMatrixLinkout = function isMatrixLinkout (title) { + return matrixLinkouts.some(linkout => linkout.title === title); }; // Return the labels from heatMap required by the specified linkout (and axis if applicable). From 6eff0fad07f155c70c01f9d3b1912c51bbc7a6ef Mon Sep 17 00:00:00 2001 From: Bradley Broom Date: Wed, 27 Aug 2025 11:56:06 -0500 Subject: [PATCH 11/11] Fixed deduplication of special coordinates --- NGCHM/WebContent/javascript/Linkout.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/NGCHM/WebContent/javascript/Linkout.js b/NGCHM/WebContent/javascript/Linkout.js index eb35a5d2..9535a6d2 100644 --- a/NGCHM/WebContent/javascript/Linkout.js +++ b/NGCHM/WebContent/javascript/Linkout.js @@ -4318,9 +4318,15 @@ var linkoutsVersion = "undefined"; .map((x) => x.replace(/\.coordinate\.\d+$/, "")) /* remove the ".coordinate." suffix */ let uniqueRowCoords = [...new Set(specialRowCoords)] /* remove duplicates */ .map((x) => ({name: x, rowOrColumn: "row"})); /* create object with name and rowOrColumn properties */ - let specialCoords = uniqueColumnCoords.concat(uniqueRowCoords); - specialCoords = [...new Set(specialCoords)]; /* remove duplicates */ - return specialCoords; + + // Remove any duplicates. + const seen = new Set(); + const unique = []; + for (const sc of uniqueColumnCoords.concat(uniqueRowCoords)) { + const key = sc.name + "|" + sc.rowOrColumn; + if (!seen.has(key)) { seen.add(key); unique.push(sc); } + } + return unique; } CUST.waitForPlugins(() => {