From a19f627c2599be00d22c1324f566d98931426649 Mon Sep 17 00:00:00 2001 From: Kirill Bessonov Date: Wed, 21 Jan 2026 15:36:26 -0500 Subject: [PATCH 01/23] fix: implement focus-locked zoom and fix root label clipping via viewBox left margin offset --- html/table.html | 70 +++++++++++++++++++++++++++++-------------------- 1 file changed, 41 insertions(+), 29 deletions(-) diff --git a/html/table.html b/html/table.html index aa1a8cf..6d47516 100644 --- a/html/table.html +++ b/html/table.html @@ -223,6 +223,7 @@ const DATA = __DEADFOOD__; const DEBUG = false; const TREE_OFFSET = 100; + const TREE_LEFT_MARGIN_VIEWBOX_OFFSET = 30; //Space to accommodate near the root inner node labels var METDATA_TABLE_NON_FULL_SCREEEN_HEIGTH = null; //before changing to full screen mode save the current size of the metadata table var NEWICK = null; var tree_root = null; @@ -628,7 +629,7 @@ SHOW_BRANCH_LENGTHS = false; var ele = document.getElementById("TreeData"); var ele_style = window.getComputedStyle(ele); - const width = parseInt(ele_style.width)-TREE_OFFSET; + const width = parseInt(ele_style.width)-TREE_OFFSET+TREE_LEFT_MARGIN_VIEWBOX_OFFSET; //var width = window.innerWidth; const marginTop = 40; @@ -722,7 +723,8 @@ const transition = svg.transition("tree-layout") .duration(duration) .attr("height", height) - .attr("viewBox", [0, left.x - marginTop, width, height]) + .attr("viewBox", [-TREE_LEFT_MARGIN_VIEWBOX_OFFSET, left.x - marginTop, + width, height]) .tween("resize", window.ResizeObserver ? null : () => () => svg.dispatch("toggle")) .on("end", () => { svg.dispatch("layout-complete");// This tells the rest of the app "The tree is done rendering!" @@ -880,7 +882,7 @@ // Made with lots of help from: https://observablehq.com/d/6c52fee38fb28b2f var ele = document.getElementById("TreeData"); var ele_style = window.getComputedStyle(ele); - const width = parseInt(ele_style.width)-TREE_OFFSET; + const width = parseInt(ele_style.width)-TREE_OFFSET+TREE_LEFT_MARGIN_VIEWBOX_OFFSET; const marginTop = 40; const marginRight = 10; const marginBottom = 30; // updated for viewing @@ -1007,7 +1009,8 @@ const transition = svg.transition("tree-layout") .duration(duration) .attr("height", height) - .attr("viewBox", [0, left.x - marginTop, width, height]) + .attr("viewBox", [-TREE_LEFT_MARGIN_VIEWBOX_OFFSET, left.x - marginTop, + width, height]) .tween("resize", window.ResizeObserver ? null : () => () => svg.dispatch("toggle")) .on("end", () => { svg.dispatch("layout-complete");// This tells the rest of the app "The tree is done rendering!" @@ -3441,14 +3444,27 @@ }); + + zoom_tree = function(){ const zoom_times = parseFloat(document.querySelector('#zoom_slider').value) const tree_data_elm = document.querySelector('#TreeData') const tree_svg = document.querySelector('#TreeSVG') document.querySelector('#zoom_slider_value').textContent = zoom_times - const new_width = ORIGINAL_WIDTH_SVG * zoom_times //use global value as getBBox() at high zooms gives wrong values - const prev_width = parseFloat(document.querySelector('#TreeSVG').style.width) + + // Define the base coordinate width (unzoomed) including your offset + const base_coordinate_width = ORIGINAL_WIDTH_SVG + TREE_LEFT_MARGIN_VIEWBOX_OFFSET + 20; + const current_vb = tree_svg.viewBox.baseVal; //current viewBox values + const base_coordinate_height = current_vb.height; //the unzoomed height of the tree. + + //Calculate new physical dimensions of width and height as both must be scaled for zoom to work + const new_width = base_coordinate_width * zoom_times + const new_height = base_coordinate_height * zoom_times + + //Get the previous physical width before zooming + const prev_width = parseFloat(document.querySelector('#TreeSVG').style.width) || base_coordinate_width; + const prev_height = parseFloat(document.querySelector('#TreeSVG').style.height) || base_coordinate_height; //Previous x and y scroll positions before applying a zoom const prev_x_scroll_pos = tree_data_elm.scrollLeft @@ -3457,31 +3473,27 @@ //document.querySelector('#TreeSVG').style.width = new_width ; //modify width of SVG tree // svg width was not being properly set on firfox document.querySelector('#TreeSVG').setAttribute("width", `${new_width}`); //modify width of SVG tree + document.querySelector('#TreeSVG').setAttribute("height", `${new_height}`); //modify height of SVG tree + + // This is what makes the tree actually look bigger inside the new width/height + tree_svg.setAttribute("viewBox", `${-TREE_LEFT_MARGIN_VIEWBOX_OFFSET} ${current_vb.y} ${base_coordinate_width} ${base_coordinate_height}`); - //store new translated x and y coordinates after zooming - let zoom_x_translated = 0 - let zoom_y_translated = 0 - const scaleChange = new_width/prev_width //sometimes the specified zoom factor is not exact, use this value instead - - //keeping zoom centered on an element - if(new_width > prev_width){ - //get previous scroll position which is the left most x-coordinate position of a view - //translate that coordiante to a new zoomed state - //add 1/4 view field length scaled to a new zoomed state coordinate - zoom_x_translated = prev_x_scroll_pos*scaleChange + (tree_data_elm.clientWidth/4)*(scaleChange) - zoom_y_translated = prev_y_scroll_pos*scaleChange + (tree_data_elm.clientHeight/4)*(scaleChange) - - }else{ - //Zooming out - //Scale current scroll position (previous scroll + 1/4 view field distance) to a new coordinate zoomed out state - //Substract the 1/4 offeset/margin that is already in the needed zoomed out state (no adjustments) - zoom_x_translated = prev_x_scroll_pos*scaleChange - (tree_data_elm.clientWidth/4) - zoom_y_translated = prev_y_scroll_pos*scaleChange - (tree_data_elm.clientHeight/4) - - } - document.querySelector('#TreeData').scrollTo(zoom_x_translated,zoom_y_translated) - } + // Calculate the scale change factor for width and height + const scaleChangeX = new_width / prev_width; + const scaleChangeY = new_height / prev_height; + + // Focus on the center of the current view for a smoother experience + // Formula: (Current center point * scale) - half of the window + const view_w = tree_data_elm.clientWidth; + const view_h = tree_data_elm.clientHeight; + + let zoom_x_translated = (prev_x_scroll_pos + view_w / 2) * scaleChangeX - (view_w / 2); + let zoom_y_translated = (prev_y_scroll_pos + view_h / 2) * scaleChangeY - (view_h / 2); + document.querySelector('#TreeData').scrollTo(zoom_x_translated, zoom_y_translated); + + } + scroll_into_view_treenode = function(id){ let selectedNode = document.querySelector(`[id='${id}']`) if(selectedNode !== null){ From c990a001a6f62408c9f987d9a40d4a1c52dfcf4c Mon Sep 17 00:00:00 2001 From: Kirill Bessonov Date: Thu, 22 Jan 2026 11:04:36 -0500 Subject: [PATCH 02/23] fix: prevent scroll feedback loop and jitter on zoom out --- html/table.html | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/html/table.html b/html/table.html index 6d47516..d6813c9 100644 --- a/html/table.html +++ b/html/table.html @@ -3488,10 +3488,22 @@ const view_w = tree_data_elm.clientWidth; const view_h = tree_data_elm.clientHeight; - let zoom_x_translated = (prev_x_scroll_pos + view_w / 2) * scaleChangeX - (view_w / 2); - let zoom_y_translated = (prev_y_scroll_pos + view_h / 2) * scaleChangeY - (view_h / 2); + // Calculate the physical "Boundaries" of the new scaled content. + const maxPossibleX = new_width - view_w; + const maxPossibleY = new_height - view_h; + + // Calculate the "Ideal" scroll target. + let targetX = (prev_x_scroll_pos + view_w / 2) * scaleChangeX - (view_w / 2); + let targetY = (prev_y_scroll_pos + view_h / 2) * scaleChangeY - (view_h / 2); + + // This prevents the browser from fighting your scrollTo command + zoom_x_translated = Math.round(Math.max(0, Math.min(targetX, maxPossibleX))); + zoom_y_translated = Math.round(Math.max(0, Math.min(targetY, maxPossibleY))); + + //Apply the final, safe coordinates preventing dancing x-axis effect on zoom out document.querySelector('#TreeData').scrollTo(zoom_x_translated, zoom_y_translated); + } scroll_into_view_treenode = function(id){ @@ -3588,9 +3600,14 @@ scroll_legend_into_view = function(node,full_screen_btn, node_TreeData){ + console.log("Left value recalculated in scroll_legend_into_view()"); + if(node !== null){ - node.style.left=`${node_TreeData.scrollLeft}px` - node.style.top=`${node_TreeData.scrollTop}px` + const x = Math.round(node_TreeData.scrollLeft); + const y = Math.round(node_TreeData.scrollTop); + + node.style.left=`${x}px` + node.style.top=`${y}px` full_screen_btn.style.right = `-${node_TreeData.scrollLeft}px` full_screen_btn.style.top = `${node_TreeData.scrollTop}px` From cadfff4cdce23f0ec0c43f1c685f752b8c2a891a Mon Sep 17 00:00:00 2001 From: Kirill Bessonov Date: Thu, 22 Jan 2026 15:56:00 -0500 Subject: [PATCH 03/23] added subtree-aware legend preservation and fixed missing metadata handling so legend, colouring and leaf nodes text are in sync with metadata source --- html/table.html | 79 +++++++++++++++++++++++++++++++------------------ 1 file changed, 51 insertions(+), 28 deletions(-) diff --git a/html/table.html b/html/table.html index d6813c9..c0fe636 100644 --- a/html/table.html +++ b/html/table.html @@ -225,6 +225,7 @@ const TREE_OFFSET = 100; const TREE_LEFT_MARGIN_VIEWBOX_OFFSET = 30; //Space to accommodate near the root inner node labels var METDATA_TABLE_NON_FULL_SCREEEN_HEIGTH = null; //before changing to full screen mode save the current size of the metadata table + var LAST_METADATA_COLUMN_SELECTED = null; //last metadata column selected for legend rendering. useful for subtree legend generation var NEWICK = null; var tree_root = null; var METADATA = null; @@ -2119,6 +2120,8 @@ // Create a subtree from the inner node selected in a tree let CustomSubTree = (event, data) => { console.debug("Subtree is being rendered") + const isLegendActive = !$('#colour-legend').hasClass('d-none'); + console.debug("Legend is active:", isLegendActive); $("#legend_toggle").bootstrapToggle('off') SelectedNodes.drawSelectedNodes(); $('#colour-legend').empty() @@ -2165,6 +2168,12 @@ svg_chart.style.paddingLeft = `${renderedTreePadding + TREE_OFFSET}px` $("#TreeData").append(svg_chart); + + if (isLegendActive && LAST_METADATA_COLUMN_SELECTED !== null) { + // Regenerate the legend for the subtree using last selected field + colour_by_element(LAST_METADATA_COLUMN_SELECTED); + } + $(".leaf-node").each((i, elm) => { // Maintain colour of already selected nodes let [parent, circle, text] = SelectedNodes.getNodeData(elm); @@ -2296,7 +2305,7 @@ } maxIDLength = Math.max(key.length, maxIDLength); const selectedValues = selected_indexes.map((i) => { - let row_label = String(row[i] || "NA").trim(); + let row_label = String(row[i] ?? "").trim(); // ?? preserves '0', empty strings, and false. It only replaces null/undefined. /** @remarks fromCodePoint retrieves the the horizontal unicode elipse so that the charactar is rendered nicely. */ @@ -2629,16 +2638,17 @@ let node_leg = $("#colour-legend") node_leg.empty() node_leg.append(`
`) - - for(const item of array_tuples){ //[field_value, hex_colour] + + for(const item of array_tuples.filter(tuple => tuple[0] !== '')){ //[field_value, hex_colour] let row_legend_node = $(`
`) let field_value=item[0] - if(field_value === ''){field_value="Not defined"} + let count = (value2samples[field_value] || []).length; + //if(field_value === ''){field_value="Not defined"} row_legend_node.append(` \n ${field_value}`) + style="padding-left:5px">${field_value} (${count})`) node_leg.append(row_legend_node) } document.querySelector('#colour-legend').classList.remove("d-none"); @@ -2824,6 +2834,7 @@ svg.selectAll("g.leaf-node").each(function () { const g = d3.select(this); const nodeId = this.id; + if (isEmpty) { newLabel = `${nodeId}`; } else { @@ -2835,6 +2846,7 @@ trimEnd() would work, but if we had more options for justifying text in the future in may create some problems. */ + newLabel = hasValue ? `${joinedFields}` : nodeId; } g.selectAll("text") @@ -3053,6 +3065,7 @@ let colour_by_element = (ele) => { + LAST_METADATA_COLUMN_SELECTED = ele; // Create a legend for the column selected if(tree_root === null){ // TODO add pop up @@ -3061,6 +3074,7 @@ } let col_index = ele.id; + $("#legend_toggle").bootstrapToggle('on') let unq_data = new Set(); @@ -3069,7 +3083,7 @@ }) - // Need to create groupings of the each set of ID's belonging to each value grouping + // Need to create GLOBAL TREE groupings of the each set of ID's belonging to each value grouping let break_down_values = Array.from(unq_data); break_down_values.sort() @@ -3134,35 +3148,45 @@ //colour each leaf nodes with the selected colour let colour_idx = 0; colour_legend.length = 0; // clear out old data, so new legend created each time + for (const nodeId of SelectedNodes.nodes) { console.debug(nodeId); // This will log each node ID in the Set } + // This local object will hold only the nodes currently in the DOM + // Relevant when generating the legend for subtrees + let visible_breakdown = []; for(const item in break_down_values){ + let nodes_found_for_this_category = []; //checks if node exists in DOM now + if (item === "" || item === null) continue; //Skip empty field values (leaves them uncoloured/hidden from legend) + break_down_values[item].forEach((x, i) => { - - try{ - let current_item = document.querySelector(`[id='${x}']`); - let [elem, circle, text] = SelectedNodes.getNodeData(current_item); - if(SelectedNodes.nodes.has(x)){ - circle.dataset.oldfill = colours[colour_idx]; - }else{ - if (circle.style.fill !== "") { - circle.dataset.oldfill = circle.style.fill; - }else{ - circle.dataset.oldfill = SelectedNodes.getDefaultColour(); - } + + //check if the node exists in the DOM (e.g. subtree case) + if(document.getElementById(x)){ + + try{ + let current_item = document.querySelector(`[id='${x}']`); + let [elem, circle, text] = SelectedNodes.getNodeData(current_item); + nodes_found_for_this_category.push(x); + circle.dataset.oldfill = circle.style.fill || SelectedNodes.getDefaultColour(); circle.style.fill = colours[colour_idx]; + + }catch(error){ + console.error("Could not find node ", x, " in DOM"); + console.error(error); } - - }catch(error){ - console.error("Could not find ", x, " In DOM"); - console.error(error); } }); - colour_legend.push([item, colours[colour_idx]]) - colour_idx++; + + + if (nodes_found_for_this_category.length > 0) { + colour_legend.push([item, colours[colour_idx]]) + visible_breakdown[item] = nodes_found_for_this_category; + colour_idx++; + } } - CreateNodeLegend(colour_legend, col_index, break_down_values); + + CreateNodeLegend(colour_legend, col_index, visible_breakdown); }; @@ -3478,7 +3502,6 @@ // This is what makes the tree actually look bigger inside the new width/height tree_svg.setAttribute("viewBox", `${-TREE_LEFT_MARGIN_VIEWBOX_OFFSET} ${current_vb.y} ${base_coordinate_width} ${base_coordinate_height}`); - // Calculate the scale change factor for width and height const scaleChangeX = new_width / prev_width; const scaleChangeY = new_height / prev_height; @@ -3492,7 +3515,7 @@ const maxPossibleX = new_width - view_w; const maxPossibleY = new_height - view_h; - // Calculate the "Ideal" scroll target. + // Calculate the theoretical scroll target that does not care if the result is possible given screen dims let targetX = (prev_x_scroll_pos + view_w / 2) * scaleChangeX - (view_w / 2); let targetY = (prev_y_scroll_pos + view_h / 2) * scaleChangeY - (view_h / 2); @@ -3500,7 +3523,7 @@ zoom_x_translated = Math.round(Math.max(0, Math.min(targetX, maxPossibleX))); zoom_y_translated = Math.round(Math.max(0, Math.min(targetY, maxPossibleY))); - //Apply the final, safe coordinates preventing dancing x-axis effect on zoom out + //Apply the final, safe coordinates preventing x-axis sliding effect on zoom out document.querySelector('#TreeData').scrollTo(zoom_x_translated, zoom_y_translated); From 9b6e3903cbddb2a1fe872a194a8b0137b4f72322 Mon Sep 17 00:00:00 2001 From: Kirill Bessonov Date: Fri, 23 Jan 2026 09:42:46 -0500 Subject: [PATCH 04/23] fix: add 15px buffer to SVG container to prevent clipping at the bottom --- html/table.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/html/table.html b/html/table.html index c0fe636..9440eb6 100644 --- a/html/table.html +++ b/html/table.html @@ -2864,7 +2864,8 @@ const newWidth = treeContainer.node().getBBox().width - const newHeight = treeContainer.node().getBBox().height + const newHeight = treeContainer.node().getBBox().height+20 // add some padding as calculates the coordinates of the paths and shapes, but it does not include the width of the strokes + const viewBoxAttr = svg.attr("viewBox") if (!viewBoxAttr) { console.warn("viewBox not yet initialized by chart function."); From 691697ca270c97c1ddb8de5225bd19f809366699 Mon Sep 17 00:00:00 2001 From: Kirill Bessonov Date: Fri, 23 Jan 2026 09:54:22 -0500 Subject: [PATCH 05/23] fix: revert back to the use of existing height viewbox value upon leaf nodes text update as tree gets wider but not taller so previous fix with padding is commented out and newHeight is not calculated and not used --- html/table.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/html/table.html b/html/table.html index 9440eb6..e0801d7 100644 --- a/html/table.html +++ b/html/table.html @@ -2874,8 +2874,8 @@ const [minX, minY, width, height] = viewBoxAttr.split(/[\s,]+/).map(Number) // Update the width of the svg element svg.attr("width", newWidth) - .attr("height", newHeight) - .attr("viewBox", `${minX} ${minY} ${newWidth} ${newHeight}`) + .attr("height", height) + .attr("viewBox", `${minX} ${minY} ${newWidth} ${height}`) svg.style.overflow = "scroll"; From 88025be135fb115da718330dcf58a1a044d1dc17 Mon Sep 17 00:00:00 2001 From: Kirill Bessonov Date: Fri, 23 Jan 2026 15:15:29 -0500 Subject: [PATCH 06/23] use data-fieldvalue attribute for node selection to support counts in labels --- html/table.html | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/html/table.html b/html/table.html index e0801d7..193d209 100644 --- a/html/table.html +++ b/html/table.html @@ -2637,17 +2637,25 @@ let node_leg = $("#colour-legend") node_leg.empty() - node_leg.append(`
`) + //Add the drag handle + node_leg.append(`
`) + + + const columnName = TABLE_HEADERS[column_index] || `Unlabeled (column #${Number(column_index)+1})` //in case no column name is provided + node_leg.append(`
+ ${columnName} +
`); + for(const item of array_tuples.filter(tuple => tuple[0] !== '')){ //[field_value, hex_colour] let row_legend_node = $(`
`) let field_value=item[0] let count = (value2samples[field_value] || []).length; - //if(field_value === ''){field_value="Not defined"} + //if(field_value === ''){field_value="Not defined"} row_legend_node.append(` \n - ${field_value} (${count})`) node_leg.append(row_legend_node) } @@ -2864,7 +2872,7 @@ const newWidth = treeContainer.node().getBBox().width - const newHeight = treeContainer.node().getBBox().height+20 // add some padding as calculates the coordinates of the paths and shapes, but it does not include the width of the strokes + //const newHeight = treeContainer.node().getBBox().height+20 // add some padding as calculates the coordinates of the paths and shapes, but it does not include the width of the strokes const viewBoxAttr = svg.attr("viewBox") if (!viewBoxAttr) { @@ -3649,7 +3657,7 @@ //find indices of filtered data in a metadata table let indexes = data_table.rows( (idx, data, node) => { - if(data[column_index] === `${legend_node.textContent}` ){ + if(data[column_index] === `${legend_node.getAttribute('data-fieldvalue')}` ){ return true }else{ return false From 5d3ef013f413f3e52f712e1dbb498c3e8673d6f3 Mon Sep 17 00:00:00 2001 From: Kirill Bessonov Date: Mon, 26 Jan 2026 09:37:45 -0500 Subject: [PATCH 07/23] Fixed the right margin crop after text label addition --- html/table.html | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/html/table.html b/html/table.html index 193d209..c922f4f 100644 --- a/html/table.html +++ b/html/table.html @@ -2871,7 +2871,7 @@ - const newWidth = treeContainer.node().getBBox().width + const newWidth = treeContainer.node().getBBox().width+20 // add some padding as calculates the coordinates of the paths and shapes, but it does not include the width of the strokes //const newHeight = treeContainer.node().getBBox().height+20 // add some padding as calculates the coordinates of the paths and shapes, but it does not include the width of the strokes const viewBoxAttr = svg.attr("viewBox") @@ -2885,8 +2885,6 @@ .attr("height", height) .attr("viewBox", `${minX} ${minY} ${newWidth} ${height}`) svg.style.overflow = "scroll"; - - } @@ -3637,7 +3635,7 @@ if(node !== null){ const x = Math.round(node_TreeData.scrollLeft); const y = Math.round(node_TreeData.scrollTop); - + node.style.left=`${x}px` node.style.top=`${y}px` full_screen_btn.style.right = `-${node_TreeData.scrollLeft}px` From 24cefdd597ceaaf8c29105eceaf69a392f047705 Mon Sep 17 00:00:00 2001 From: Kirill Bessonov Date: Mon, 26 Jan 2026 17:54:49 -0500 Subject: [PATCH 08/23] fixes of the legend dragging while in scroll position --- html/table.html | 39 ++++++++++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/html/table.html b/html/table.html index c922f4f..027bacc 100644 --- a/html/table.html +++ b/html/table.html @@ -235,8 +235,8 @@ var ORIGINAL_DATA = null; var ORIGINAL_VIEW_BOX = null; var ORIGINAL_WIDTH_SVG = 0; - var LAST_MOVE_X = 0; - var LAST_MOVE_Y = 0; + var LAST_SCROLL_X = 0; // Store last scroll x-position making legend position updates smarter + var LAST_SCROLL_Y = 0; // Store last scroll y-position making legend position updates smarter var SHOW_BRANCH_LENGTHS = true; var SHOW_METADATA_IN_NAME = false; var LINE_THICKNESS = 2.0; @@ -2816,6 +2816,7 @@ const svg = d3.select("#TreeSVG") const treeContainer = svg.select('g[cursor="pointer"][pointer-events="all"]') + const treeDataContainer = document.querySelector('#TreeData'); const svgAttrWidth = svg.attr("width") const oldWidth = treeContainer.node().getBBox().width @@ -2871,7 +2872,7 @@ - const newWidth = treeContainer.node().getBBox().width+20 // add some padding as calculates the coordinates of the paths and shapes, but it does not include the width of the strokes + const newWidth = treeContainer.node().getBBox().width+50 // add some padding as calculates the coordinates of the paths and shapes, but it does not include the width of the strokes //const newHeight = treeContainer.node().getBBox().height+20 // add some padding as calculates the coordinates of the paths and shapes, but it does not include the width of the strokes const viewBoxAttr = svg.attr("viewBox") @@ -2881,11 +2882,18 @@ } const [minX, minY, width, height] = viewBoxAttr.split(/[\s,]+/).map(Number) // Update the width of the svg element + //treeDataContainer.scrollLeft = newWidth; + console.log("The scroll left BEFORE is set to:", treeDataContainer.scrollLeft); + svg.attr("width", newWidth) .attr("height", height) .attr("viewBox", `${minX} ${minY} ${newWidth} ${height}`) svg.style.overflow = "scroll"; + const maxScrollLeft = newWidth - treeDataContainer.clientWidth; + treeDataContainer.scrollLeft = maxScrollLeft; //scroll to the right margin + + console.log("The scroll left AFTER is set to:", treeDataContainer.scrollLeft); } @@ -3633,8 +3641,16 @@ console.log("Left value recalculated in scroll_legend_into_view()"); if(node !== null){ - const x = Math.round(node_TreeData.scrollLeft); - const y = Math.round(node_TreeData.scrollTop); + const x = Math.round(node_TreeData.scrollLeft+10); + const y = Math.round(node_TreeData.scrollTop+10); + + // Skip if the movement is less than 5px (small) + if (Math.abs(x - LAST_SCROLL_X) < 5 && Math.abs(y - LAST_SCROLL_Y) < 2) { + return; + } + + LAST_SCROLL_X = x; + LAST_SCROLL_Y = y; node.style.left=`${x}px` node.style.top=`${y}px` @@ -4184,6 +4200,12 @@ dragDivByMouse = function(event) { const legend_div = document.querySelector('#colour-legend'); const control_panel_div = document.querySelector("#control_panel"); + const treeDataContainer = document.querySelector('#TreeData'); + + //Current SVG TreeData scroll positions to adjust for scrolling during drag + const scrollY = treeDataContainer.scrollTop; + const scrollX = treeDataContainer.scrollLeft; + // Get bounding box of the legend const rect = legend_div.getBoundingClientRect(); @@ -4192,6 +4214,9 @@ const grabOffsetX = event.clientX - rect.left; const grabOffsetY = event.clientY - rect.top; + + const tree = document.querySelector('#TreeData'); + // Store control panel width let controlPanelOffset = control_panel_div.offsetWidth; // Override offset if document is in fullscreen mode @@ -4206,8 +4231,8 @@ e.preventDefault(); // Calculate new position, accounting for grab offset and control panel width - const newLeft = e.clientX - grabOffsetX - controlPanelOffset; - const newTop = e.clientY - grabOffsetY; + const newLeft = e.clientX - grabOffsetX - controlPanelOffset + scrollX; + const newTop = e.clientY - grabOffsetY + scrollY; legend_div.style.left = newLeft + "px"; legend_div.style.top = newTop + "px"; From 8f0cfdce173bc7fb7bf119ef3e2c086cc7b0b4c3 Mon Sep 17 00:00:00 2001 From: Kirill Bessonov Date: Tue, 27 Jan 2026 02:03:01 -0500 Subject: [PATCH 09/23] fix: resolve UI layout overflow and vertical resize jumping --- html/table.html | 65 +++++++++++++++++++++++++++++++++++++------------ 1 file changed, 50 insertions(+), 15 deletions(-) diff --git a/html/table.html b/html/table.html index 027bacc..bf4190a 100644 --- a/html/table.html +++ b/html/table.html @@ -2,6 +2,15 @@ + + ArborView @@ -2743,15 +2745,30 @@ }; let SubsetTable = () => { + let totalSamples = data_table.rows().count();; let filtered = data_table.data().filter((value, idx) => { return SelectedNodes.nodes.has(value[ID_FIELD]); }); + let remainingSamples = filtered.length; + let filteredOutSamplesCount = totalSamples - remainingSamples; data_table.clear(); data_table.rows.add(filtered).draw(); $("td").addClass("editable"); $("td:not(:first-child)").attr("contenteditable", true); + Swal.fire({ + title: 'Metadata Table Subsetted based on Selected Nodes', + html: `${remainingSamples} samples kept out of ${totalSamples}.
` + + `${filteredOutSamplesCount} samples were filtered out.`, + icon: 'success', + timer: 5000, + timerProgressBar: true, + showConfirmButton: false, + toast: true, + position: 'top-end' + }); + }; From eb9cc002973b1b0bffc9de392e9d49431905643a Mon Sep 17 00:00:00 2001 From: Kirill Bessonov Date: Fri, 30 Jan 2026 15:47:50 -0500 Subject: [PATCH 13/23] fix sticky horizontal positioning for metadata filter and controls and full screen button so when we scroll horizontally the search box and buttons will move too --- html/table.html | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/html/table.html b/html/table.html index f71d32f..3a2e670 100644 --- a/html/table.html +++ b/html/table.html @@ -35,6 +35,16 @@ flex: 0 0 5px !important; min-height: 5px; } + + #metadata_wrapper{ + width:fit-content; + } + + #metadata_filter, #metadata_paginate{ + position: sticky; + right: 0; + } + .tree-svg { height: auto; @@ -137,7 +147,6 @@ .dataTables_filter { position: relative; - margin-right: 24px; } .dataTables_wrapper .dataTables_filter input{ border-radius: 20px !important; @@ -2492,6 +2501,8 @@ //icon.id="tree_fullscreen_btn" //icon.onclick="tree_full_screen_mode()" + $('#metadata_length').addClass('p-1'); + let html=document.createElement("div"); html.className = "paginate_button displayAll" html.id = "paginate_button_all" @@ -2500,15 +2511,6 @@ document.querySelector("#metadata_paginate").appendChild(html) } - let maximize_button_html=document.createElement("i"); - maximize_button_html.style = "position:absolute; top:0; right: 0; cursor: pointer; color:blue; margin-right:1px" - maximize_button_html.className="d-flex fs-5 bi-arrow-up-right-square" - maximize_button_html.id = "metadata_fullscreen_btn" - maximize_button_html.setAttribute("onclick", "metadata_full_screen_mode()") - if(document.querySelector("#metadata_fullscreen_btn") === null){ - document.querySelector("#metadata_wrapper").appendChild(maximize_button_html) - } - $('.paginate_button.displayAll:not(.disabled)', this.api().table().container()) .on('click', function(){ @@ -2563,6 +2565,15 @@ }); $('#metadata_filter').append(clearBtn); + + let maximize_button_html=document.createElement("i"); + maximize_button_html.style = "cursor: pointer; color:blue; margin-right:1px" + maximize_button_html.className="d-inline-flex p-1 fs-5 bi-arrow-up-right-square" + maximize_button_html.id = "metadata_fullscreen_btn" + maximize_button_html.setAttribute("onclick", "metadata_full_screen_mode()") + if(document.querySelector("#metadata_fullscreen_btn") === null){ + document.querySelector("#metadata_filter").appendChild(maximize_button_html); + } // TODO initializing table from json object would minimize copies // Saving data as a map, to allow for updating of metadata @@ -4031,8 +4042,6 @@ document.addEventListener("DOMContentLoaded",()=>{ - let button = document.querySelector('#tree_fullscreen_btn') - /** * Handles dynamic layout adjustments when the #ClusterInfo section is shown or hidden. * From 8c73222c285a1d5ebd2e97a8053b81cc3b4d2ad5 Mon Sep 17 00:00:00 2001 From: Kirill Bessonov Date: Sun, 1 Feb 2026 09:05:11 -0500 Subject: [PATCH 14/23] make metadata table controls sticky on horizonatal axes. Fixed horizontal position of the show entries and clear filters and search bar elements in metadata table on the horizontal scroll so they are visible upon scrolling if table is wider than a view dimensions. They still disappear on vertical scroll --- html/table.html | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/html/table.html b/html/table.html index 3a2e670..486c80a 100644 --- a/html/table.html +++ b/html/table.html @@ -40,10 +40,15 @@ width:fit-content; } - #metadata_filter, #metadata_paginate{ + #metadata_filter, #metadata_paginate{ /*Search and clear filters button of metadata table */ position: sticky; right: 0; } + + #metadata_length { /*show x entries filter of metadata table */ + position: sticky; + left: 0; + } .tree-svg { From d9bc2475dbc792c11ad263dae62cd77b402dde77 Mon Sep 17 00:00:00 2001 From: Kirill Bessonov Date: Mon, 2 Feb 2026 15:43:48 -0500 Subject: [PATCH 15/23] Fixed raw data representation in leaf nodes text. We now use DOM textContent for ORIGINAL_DATA to fix symbol rendering Bypasses DataTables' automatic HTML escaping. This ensures that special symbols render correctly in the tree leaf nodes text and vertical pipe alignment remains consistent --- html/table.html | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/html/table.html b/html/table.html index 486c80a..880d9f5 100644 --- a/html/table.html +++ b/html/table.html @@ -2581,12 +2581,26 @@ } // TODO initializing table from json object would minimize copies - // Saving data as a map, to allow for updating of metadata + + /** + * INITIALIZE ORIGINAL_DATA MAP - Saving data as a map, to allow for updating of metadata + * * We iterate through the DataTables API to build a global Map. + * Instead of using .data(), we access the DOM nodes directly to extract + * .textContent. This prevents symbols like '&' from being stored as '&'. + */ ORIGINAL_DATA = new Map(); - data_table.rows().data().each((value, index) => { - ORIGINAL_DATA.set(value[ID_FIELD], value) - }); + data_table.rows().every(function (rowIdx, tableLoop, rowLoop) { + // Get the actual row DOM element for this row + const rowNode = this.node(); + + // Extract the textContent from each cell + const rawValues = Array.from(rowNode.cells).map(td => td.textContent.trim()); + // Store raw metadata array as a Map based on first column treated as sampleID + // Assuming ID_FIELD is the index of your ID column + ORIGINAL_DATA.set(rawValues[ID_FIELD], rawValues); + }); + // TODO This can likely be initialized in the above loop pushing to value for(const [key, value] of ORIGINAL_DATA.entries()){ @@ -2652,7 +2666,7 @@ table_body.push("\n"); } - parsed_data = null; // mark old data for GC + parsed_data = null; // mark old data for GC and cleanup memory $(table_id).html("" + table_head + "" + "" + table_body.join() + ""); // ? A bit odd we are making the table with HTML then converting to jquery datatable... CreateDataTable(); @@ -3439,6 +3453,7 @@ } METADATA = d3.tsvParse(DATA, blankQuitter); TABLE_HEADERS = METADATA.columns; + CreateTable(METADATA, "#metadata", TABLE_HEADERS); METADATA = null; initialize_legend_menu(); @@ -3463,6 +3478,7 @@ } METADATA = d3.tsvParse(metadata, blankQuitter); TABLE_HEADERS = METADATA.columns; + CreateTable(METADATA, "#metadata", TABLE_HEADERS); METADATA = null; initialize_legend_menu(); From 0678d2c24ec3b8fa903e6a340c48eb345125a094 Mon Sep 17 00:00:00 2001 From: Kirill Bessonov Date: Mon, 2 Feb 2026 16:22:52 -0500 Subject: [PATCH 16/23] Small fixed to ensure SVG respects spacing in leaf nodes and that metadata export tips correctly identify the functions to export full or truncated data --- html/table.html | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/html/table.html b/html/table.html index 880d9f5..a049056 100644 --- a/html/table.html +++ b/html/table.html @@ -53,7 +53,7 @@ .tree-svg { height: auto; - font 10px sans-serif; + font: 10px sans-serif; user-select: auto; overflow: visible; margin-left: 1em; @@ -2938,6 +2938,7 @@ // Only update text elements that start with the nodeId return d3.select(this).text().startsWith(nodeId); }) + .attr("xml:space", "preserve") // Prevents SVG from collapsing your padding spaces .text(newLabel).attr("class", "leaf-node-label"); }); if (missingMetadataNodes.size > 0) { @@ -3950,6 +3951,10 @@ downloadLink.download = 'tree_snapshot_'+date.getDate()+'-'+ (date.getMonth()+1)+'-'+date.getFullYear()+'_'+date.getHours()+'h.svg'; let svg = document.getElementById("TreeData").cloneNode(true) + let svgElement = svg.querySelector("#TreeSVG"); + svgElement.style.fontFamily = "monospace"; + + //remove duplicated leaf nodes text duplication for more accurate searches svg.querySelectorAll('g .tree-node').forEach(node => { text_nodes_leafs = node.querySelectorAll('text[class=""]') @@ -4431,12 +4436,12 @@ Export Meta Table