From 545f327a199f3a0e2564c0c0f47193496302f337 Mon Sep 17 00:00:00 2001 From: ankurjuneja Date: Thu, 14 May 2026 17:30:28 -0700 Subject: [PATCH 1/5] Click/drag zooming for QC plots --- webapp/TargetedMS/css/qcTrendPlotReport.css | 21 +++ webapp/TargetedMS/js/QCPlotHelperBase.js | 138 ++++++++++++++++++++ webapp/TargetedMS/js/QCTrendPlotPanel.js | 27 +++- 3 files changed, 184 insertions(+), 2 deletions(-) diff --git a/webapp/TargetedMS/css/qcTrendPlotReport.css b/webapp/TargetedMS/css/qcTrendPlotReport.css index b4434fed4..3b9381cd7 100644 --- a/webapp/TargetedMS/css/qcTrendPlotReport.css +++ b/webapp/TargetedMS/css/qcTrendPlotReport.css @@ -115,4 +115,25 @@ .qc-combined-tree-legend .qc-tree-precursor:hover { background-color: #f0f0f0; +} + +.y-zoom-overlay { + cursor: ns-resize; +} + +.y-zoom-selection { + fill: rgba(20, 204, 201, 0.3); + stroke: rgba(20, 204, 201, 1); + stroke-width: 1px; +} + +.y-zoom-buttons g { + cursor: pointer; +} + +.qc-reset-zoom-link { + fill: #126495; + font-size: 11px; + cursor: pointer; + text-decoration: underline; } \ No newline at end of file diff --git a/webapp/TargetedMS/js/QCPlotHelperBase.js b/webapp/TargetedMS/js/QCPlotHelperBase.js index 85cacafa4..5a902309f 100644 --- a/webapp/TargetedMS/js/QCPlotHelperBase.js +++ b/webapp/TargetedMS/js/QCPlotHelperBase.js @@ -805,6 +805,11 @@ Ext4.define("LABKEY.targetedms.QCPlotHelperBase", { } Ext4.apply(trendLineProps, this.getPlotTypeProperties(combinePlotData, plotType, isCUSUMMean, metricProps)); + var yZoomDomainCombined = this.getYZoomDomain ? this.getYZoomDomain(id) : null; + if (yZoomDomainCombined) { + trendLineProps.yZoomDomain = yZoomDomainCombined; + } + // Suppress the mean line for multi-series plots trendLineProps.mean = undefined; @@ -860,6 +865,7 @@ Ext4.define("LABKEY.targetedms.QCPlotHelperBase", { const plot = LABKEY.vis.TrendingLinePlot(plotConfig); plot.render(); + this.addYZoomInteraction(plot, id); this.attachCombinedLegendClickHandlers(); this.addAnnotationsToPlot(plot, combinePlotData); @@ -945,6 +951,11 @@ Ext4.define("LABKEY.targetedms.QCPlotHelperBase", { Ext4.apply(trendLineProps, this.getPlotTypeProperties(precursorInfo, plotType, isCUSUMMean, metricProps)); + var yZoomDomain = this.getYZoomDomain ? this.getYZoomDomain(id) : null; + if (yZoomDomain) { + trendLineProps.yZoomDomain = yZoomDomain; + } + var plotLegendData = this.getAdditionalPlotLegend(plotType); if (Ext4.isArray(this.legendData)) { plotLegendData = plotLegendData.concat(this.legendData); @@ -1022,6 +1033,7 @@ Ext4.define("LABKEY.targetedms.QCPlotHelperBase", { const plot = LABKEY.vis.TrendingLinePlot(plotConfig); plot.render(); + this.addYZoomInteraction(plot, id); this.addAnnotationsToPlot(plot, precursorInfo); this.addGuideSetTrainingRangeToPlot(plot, precursorInfo); @@ -1065,5 +1077,131 @@ Ext4.define("LABKEY.targetedms.QCPlotHelperBase", { showInPlotLegends: function () { return true; + }, + + addYZoomInteraction: function(plot, plotId) { + var me = this; + var svg = this.getSvgElForPlot(plot); + var grid = plot.grid; + + if (!plot.scales.yLeft || !plot.scales.yLeft.scale || !plot.scales.yLeft.scale.invert) { + return; + } + + var yScale = plot.scales.yLeft.scale; + var overlayWidth = grid.leftEdge - 2; + var gridTop = grid.topEdge; + var gridBottom = grid.bottomEdge; + + var dragStartY = null; + var dragCurrentY = null; + var selectionRect = null; + var zoomButtonGroup = null; + + var clampY = function(y) { + return Math.max(gridTop, Math.min(gridBottom, y)); + }; + + var removeOverlays = function() { + if (selectionRect) { + selectionRect.remove(); + selectionRect = null; + } + if (zoomButtonGroup) { + zoomButtonGroup.remove(); + zoomButtonGroup = null; + } + }; + + var drag = d3.behavior.drag() + .on('dragstart', function() { + dragStartY = clampY(d3.mouse(svg.node())[1]); + dragCurrentY = dragStartY; + removeOverlays(); + }) + .on('drag', function() { + dragCurrentY = clampY(d3.mouse(svg.node())[1]); + + var y1 = Math.min(dragStartY, dragCurrentY); + var y2 = Math.max(dragStartY, dragCurrentY); + var h = y2 - y1; + + if (h < 1) { return; } + + if (selectionRect) { + selectionRect.attr('y', y1).attr('height', h); + } + else { + selectionRect = svg.append('rect') + .attr('class', 'y-zoom-selection') + .attr('x', 1) + .attr('y', y1) + .attr('width', overlayWidth - 2) + .attr('height', h) + .style('pointer-events', 'none'); + } + }) + .on('dragend', function() { + var y1 = Math.min(dragStartY, dragCurrentY); + var y2 = Math.max(dragStartY, dragCurrentY); + + if (y2 - y1 < 5) { + removeOverlays(); + return; + } + + // SVG y is inverted: smaller pixel y = larger domain value + var domainMax = yScale.invert(y1); + var domainMin = yScale.invert(y2); + + var yMid = y1 + (y2 - y1) / 2; + var xMid = overlayWidth / 2; + + zoomButtonGroup = svg.append('g').attr('class', 'y-zoom-buttons'); + + var makeBtn = function(text, xLeft, width, onClick) { + var btnG = zoomButtonGroup.append('g'); + btnG.append('rect') + .attr('x', xLeft).attr('y', yMid - 10).attr('rx', 5).attr('ry', 5) + .attr('width', width).attr('height', 20) + .style({'fill': '#ffffff', 'stroke': '#b4b4b4'}); + btnG.append('text') + .text(text) + .attr('x', xLeft + 5).attr('y', yMid + 4) + .style({'fill': '#126495', 'font-size': '10px', 'font-weight': 'bold', + 'text-transform': 'uppercase', 'pointer-events': 'none'}); + btnG.on('click', onClick); + return btnG; + }; + + makeBtn('Zoom', xMid - 57, 50, function() { + removeOverlays(); + me.applyYZoom(plotId, domainMin, domainMax); + }); + + makeBtn('Cancel', xMid + 3, 55, function() { + removeOverlays(); + }); + }); + + svg.append('rect') + .attr('class', 'y-zoom-overlay') + .attr('x', 0) + .attr('y', gridTop) + .attr('width', overlayWidth) + .attr('height', gridBottom - gridTop) + .style('fill', 'transparent') + .call(drag); + + if (this.getYZoomDomain && this.getYZoomDomain(plotId)) { + svg.append('text') + .attr('class', 'qc-reset-zoom-link') + .text('Reset Zoom') + .attr('x', grid.leftEdge + 5) + .attr('y', grid.topEdge - 5) + .on('click', function() { + me.resetYZoom(plotId); + }); + } } }); \ No newline at end of file diff --git a/webapp/TargetedMS/js/QCTrendPlotPanel.js b/webapp/TargetedMS/js/QCTrendPlotPanel.js index 55ae8f4e2..5092f8958 100644 --- a/webapp/TargetedMS/js/QCTrendPlotPanel.js +++ b/webapp/TargetedMS/js/QCTrendPlotPanel.js @@ -82,6 +82,7 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { trailingRuns: null, minWidth: 1250, // Keep in sync with the width defined in qcTrendPlot.jsp width: '100%', + yZoomByPlot: {}, SHOW_ALL_IN_A_SINGLE_PLOT: 'Show all series in a single plot', LABEL_WIDTH: 115, @@ -1213,8 +1214,10 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { Ext4.get(this.plotDivId).mask("Loading..."); }, - displayTrendPlot: function() { - + displayTrendPlot: function(preserveZoom) { + if (!preserveZoom) { + this.yZoomByPlot = {}; + } this.setBrushingEnabled(false); this.updateSelectedAnnotations(); this.setLoadingMsg(); @@ -2223,6 +2226,26 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { } }, + getYZoomDomain: function(plotId) { + return this.yZoomByPlot && this.yZoomByPlot[plotId] ? this.yZoomByPlot[plotId] : null; + }, + + applyYZoom: function(plotId, yMin, yMax) { + if (!this.yZoomByPlot) { + this.yZoomByPlot = {}; + } + this.yZoomByPlot[plotId] = [yMin, yMax]; + this.displayTrendPlot(true /* preserveZoom */); + // TODO: add server-side metric tracking action (targetedms/trackQCPlotAction.api) + }, + + resetYZoom: function(plotId) { + if (this.yZoomByPlot) { + delete this.yZoomByPlot[plotId]; + } + this.displayTrendPlot(true /* preserveZoom */); + }, + getSvgElForPlot : function(plot) { return d3.select('#' + plot.renderTo + ' svg'); }, From 246462c59eea12a127b2ca740de18b7691c27652 Mon Sep 17 00:00:00 2001 From: ankurjuneja Date: Wed, 20 May 2026 16:12:09 -0700 Subject: [PATCH 2/5] add border rectangle when zooming --- webapp/TargetedMS/js/QCPlotHelperBase.js | 34 ++++++++++++++++++++---- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/webapp/TargetedMS/js/QCPlotHelperBase.js b/webapp/TargetedMS/js/QCPlotHelperBase.js index 5a902309f..e11f8af6b 100644 --- a/webapp/TargetedMS/js/QCPlotHelperBase.js +++ b/webapp/TargetedMS/js/QCPlotHelperBase.js @@ -1155,7 +1155,7 @@ Ext4.define("LABKEY.targetedms.QCPlotHelperBase", { var domainMin = yScale.invert(y2); var yMid = y1 + (y2 - y1) / 2; - var xMid = overlayWidth / 2; + var btnLeft = grid.leftEdge + 5; zoomButtonGroup = svg.append('g').attr('class', 'y-zoom-buttons'); @@ -1174,12 +1174,12 @@ Ext4.define("LABKEY.targetedms.QCPlotHelperBase", { return btnG; }; - makeBtn('Zoom', xMid - 57, 50, function() { + makeBtn('Zoom', btnLeft, 50, function() { removeOverlays(); me.applyYZoom(plotId, domainMin, domainMax); }); - makeBtn('Cancel', xMid + 3, 55, function() { + makeBtn('Cancel', btnLeft + 60, 55, function() { removeOverlays(); }); }); @@ -1194,11 +1194,35 @@ Ext4.define("LABKEY.targetedms.QCPlotHelperBase", { .call(drag); if (this.getYZoomDomain && this.getYZoomDomain(plotId)) { + var gridLeft = grid.leftEdge; + var gridRight = grid.rightEdge; + var gridWidth = gridRight - gridLeft; + var gridHeight = gridBottom - gridTop; + var clipId = (plot.renderTo || plotId) + '-yzoom-clip'; + + var svgDefs = svg.select('defs'); + if (svgDefs.empty()) { + svgDefs = svg.insert('defs', ':first-child'); + } + svgDefs.append('clipPath') + .attr('id', clipId) + .append('rect') + .attr('x', gridLeft).attr('y', gridTop) + .attr('width', gridWidth).attr('height', gridHeight); + + svg.selectAll('g.layer').attr('clip-path', 'url(#' + clipId + ')'); + + svg.append('rect') + .attr('class', 'y-zoom-border') + .attr('x', gridLeft).attr('y', gridTop) + .attr('width', gridWidth).attr('height', gridHeight) + .style({'fill': 'none', 'stroke': '#888', 'stroke-width': '1px', 'pointer-events': 'none'}); + svg.append('text') .attr('class', 'qc-reset-zoom-link') .text('Reset Zoom') - .attr('x', grid.leftEdge + 5) - .attr('y', grid.topEdge - 5) + .attr('x', gridLeft + 5) + .attr('y', gridTop - 5) .on('click', function() { me.resetYZoom(plotId); }); From 4c6f034232037cf78bb9424fbfb7d8463fed5fdc Mon Sep 17 00:00:00 2001 From: ankurjuneja Date: Wed, 20 May 2026 16:21:49 -0700 Subject: [PATCH 3/5] change the mouse pointer and color the entire zoom area when selecting the range on y-axis --- webapp/TargetedMS/css/qcTrendPlotReport.css | 2 +- webapp/TargetedMS/js/QCPlotHelperBase.js | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/webapp/TargetedMS/css/qcTrendPlotReport.css b/webapp/TargetedMS/css/qcTrendPlotReport.css index 3b9381cd7..da996694b 100644 --- a/webapp/TargetedMS/css/qcTrendPlotReport.css +++ b/webapp/TargetedMS/css/qcTrendPlotReport.css @@ -118,7 +118,7 @@ } .y-zoom-overlay { - cursor: ns-resize; + cursor: zoom-in; } .y-zoom-selection { diff --git a/webapp/TargetedMS/js/QCPlotHelperBase.js b/webapp/TargetedMS/js/QCPlotHelperBase.js index e11f8af6b..72ba4c2f7 100644 --- a/webapp/TargetedMS/js/QCPlotHelperBase.js +++ b/webapp/TargetedMS/js/QCPlotHelperBase.js @@ -1092,6 +1092,8 @@ Ext4.define("LABKEY.targetedms.QCPlotHelperBase", { var overlayWidth = grid.leftEdge - 2; var gridTop = grid.topEdge; var gridBottom = grid.bottomEdge; + var gridLeft = grid.leftEdge; + var gridRight = grid.rightEdge; var dragStartY = null; var dragCurrentY = null; @@ -1134,9 +1136,9 @@ Ext4.define("LABKEY.targetedms.QCPlotHelperBase", { else { selectionRect = svg.append('rect') .attr('class', 'y-zoom-selection') - .attr('x', 1) + .attr('x', gridLeft) .attr('y', y1) - .attr('width', overlayWidth - 2) + .attr('width', gridRight - gridLeft) .attr('height', h) .style('pointer-events', 'none'); } @@ -1194,8 +1196,6 @@ Ext4.define("LABKEY.targetedms.QCPlotHelperBase", { .call(drag); if (this.getYZoomDomain && this.getYZoomDomain(plotId)) { - var gridLeft = grid.leftEdge; - var gridRight = grid.rightEdge; var gridWidth = gridRight - gridLeft; var gridHeight = gridBottom - gridTop; var clipId = (plot.renderTo || plotId) + '-yzoom-clip'; @@ -1222,7 +1222,7 @@ Ext4.define("LABKEY.targetedms.QCPlotHelperBase", { .attr('class', 'qc-reset-zoom-link') .text('Reset Zoom') .attr('x', gridLeft + 5) - .attr('y', gridTop - 5) + .attr('y', gridTop - 18) .on('click', function() { me.resetYZoom(plotId); }); From ceb40f18c3896c4a05f55d6a0751c08da1048b35 Mon Sep 17 00:00:00 2001 From: ankurjuneja Date: Wed, 20 May 2026 16:25:44 -0700 Subject: [PATCH 4/5] use let and const --- webapp/TargetedMS/js/QCPlotHelperBase.js | 66 ++++++++++++------------ 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/webapp/TargetedMS/js/QCPlotHelperBase.js b/webapp/TargetedMS/js/QCPlotHelperBase.js index 72ba4c2f7..e71bf4779 100644 --- a/webapp/TargetedMS/js/QCPlotHelperBase.js +++ b/webapp/TargetedMS/js/QCPlotHelperBase.js @@ -805,7 +805,7 @@ Ext4.define("LABKEY.targetedms.QCPlotHelperBase", { } Ext4.apply(trendLineProps, this.getPlotTypeProperties(combinePlotData, plotType, isCUSUMMean, metricProps)); - var yZoomDomainCombined = this.getYZoomDomain ? this.getYZoomDomain(id) : null; + let yZoomDomainCombined = this.getYZoomDomain ? this.getYZoomDomain(id) : null; if (yZoomDomainCombined) { trendLineProps.yZoomDomain = yZoomDomainCombined; } @@ -951,7 +951,7 @@ Ext4.define("LABKEY.targetedms.QCPlotHelperBase", { Ext4.apply(trendLineProps, this.getPlotTypeProperties(precursorInfo, plotType, isCUSUMMean, metricProps)); - var yZoomDomain = this.getYZoomDomain ? this.getYZoomDomain(id) : null; + let yZoomDomain = this.getYZoomDomain ? this.getYZoomDomain(id) : null; if (yZoomDomain) { trendLineProps.yZoomDomain = yZoomDomain; } @@ -1080,31 +1080,31 @@ Ext4.define("LABKEY.targetedms.QCPlotHelperBase", { }, addYZoomInteraction: function(plot, plotId) { - var me = this; - var svg = this.getSvgElForPlot(plot); - var grid = plot.grid; + let me = this; + let svg = this.getSvgElForPlot(plot); + let grid = plot.grid; if (!plot.scales.yLeft || !plot.scales.yLeft.scale || !plot.scales.yLeft.scale.invert) { return; } - var yScale = plot.scales.yLeft.scale; - var overlayWidth = grid.leftEdge - 2; - var gridTop = grid.topEdge; - var gridBottom = grid.bottomEdge; - var gridLeft = grid.leftEdge; - var gridRight = grid.rightEdge; + let yScale = plot.scales.yLeft.scale; + let overlayWidth = grid.leftEdge - 2; + let gridTop = grid.topEdge; + let gridBottom = grid.bottomEdge; + let gridLeft = grid.leftEdge; + let gridRight = grid.rightEdge; - var dragStartY = null; - var dragCurrentY = null; - var selectionRect = null; - var zoomButtonGroup = null; + let dragStartY = null; + let dragCurrentY = null; + let selectionRect = null; + let zoomButtonGroup = null; - var clampY = function(y) { + let clampY = function(y) { return Math.max(gridTop, Math.min(gridBottom, y)); }; - var removeOverlays = function() { + let removeOverlays = function() { if (selectionRect) { selectionRect.remove(); selectionRect = null; @@ -1115,7 +1115,7 @@ Ext4.define("LABKEY.targetedms.QCPlotHelperBase", { } }; - var drag = d3.behavior.drag() + let drag = d3.behavior.drag() .on('dragstart', function() { dragStartY = clampY(d3.mouse(svg.node())[1]); dragCurrentY = dragStartY; @@ -1124,9 +1124,9 @@ Ext4.define("LABKEY.targetedms.QCPlotHelperBase", { .on('drag', function() { dragCurrentY = clampY(d3.mouse(svg.node())[1]); - var y1 = Math.min(dragStartY, dragCurrentY); - var y2 = Math.max(dragStartY, dragCurrentY); - var h = y2 - y1; + let y1 = Math.min(dragStartY, dragCurrentY); + let y2 = Math.max(dragStartY, dragCurrentY); + let h = y2 - y1; if (h < 1) { return; } @@ -1144,8 +1144,8 @@ Ext4.define("LABKEY.targetedms.QCPlotHelperBase", { } }) .on('dragend', function() { - var y1 = Math.min(dragStartY, dragCurrentY); - var y2 = Math.max(dragStartY, dragCurrentY); + let y1 = Math.min(dragStartY, dragCurrentY); + let y2 = Math.max(dragStartY, dragCurrentY); if (y2 - y1 < 5) { removeOverlays(); @@ -1153,16 +1153,16 @@ Ext4.define("LABKEY.targetedms.QCPlotHelperBase", { } // SVG y is inverted: smaller pixel y = larger domain value - var domainMax = yScale.invert(y1); - var domainMin = yScale.invert(y2); + let domainMax = yScale.invert(y1); + let domainMin = yScale.invert(y2); - var yMid = y1 + (y2 - y1) / 2; - var btnLeft = grid.leftEdge + 5; + let yMid = y1 + (y2 - y1) / 2; + let btnLeft = grid.leftEdge + 5; zoomButtonGroup = svg.append('g').attr('class', 'y-zoom-buttons'); - var makeBtn = function(text, xLeft, width, onClick) { - var btnG = zoomButtonGroup.append('g'); + let makeBtn = function(text, xLeft, width, onClick) { + let btnG = zoomButtonGroup.append('g'); btnG.append('rect') .attr('x', xLeft).attr('y', yMid - 10).attr('rx', 5).attr('ry', 5) .attr('width', width).attr('height', 20) @@ -1196,11 +1196,11 @@ Ext4.define("LABKEY.targetedms.QCPlotHelperBase", { .call(drag); if (this.getYZoomDomain && this.getYZoomDomain(plotId)) { - var gridWidth = gridRight - gridLeft; - var gridHeight = gridBottom - gridTop; - var clipId = (plot.renderTo || plotId) + '-yzoom-clip'; + let gridWidth = gridRight - gridLeft; + let gridHeight = gridBottom - gridTop; + let clipId = (plot.renderTo || plotId) + '-yzoom-clip'; - var svgDefs = svg.select('defs'); + let svgDefs = svg.select('defs'); if (svgDefs.empty()) { svgDefs = svg.insert('defs', ':first-child'); } From c82ebb754fd4f552fd23df24abf771c515533455 Mon Sep 17 00:00:00 2001 From: ankurjuneja Date: Wed, 20 May 2026 17:00:28 -0700 Subject: [PATCH 5/5] add test and testing support for y-axis zoom --- .../components/targetedms/QCPlotsWebPart.java | 40 ++++++++++++++ .../tests/targetedms/TargetedMSQCTest.java | 55 +++++++++++++++++++ 2 files changed, 95 insertions(+) diff --git a/test/src/org/labkey/test/components/targetedms/QCPlotsWebPart.java b/test/src/org/labkey/test/components/targetedms/QCPlotsWebPart.java index 1a2792f5c..fcc5c666c 100644 --- a/test/src/org/labkey/test/components/targetedms/QCPlotsWebPart.java +++ b/test/src/org/labkey/test/components/targetedms/QCPlotsWebPart.java @@ -904,6 +904,42 @@ public String toString() } } + public void performYAxisZoom(QCPlot qcPlot) + { + WebElement plotEl = qcPlot.getPlot(); + WebElement overlay = elementCache().yZoomOverlay.findElement(plotEl); + getWrapper().scrollIntoView(overlay); + + int dragOffset = 50; + new Actions(getWrapper().getDriver()) + .moveToElement(overlay, 0, -dragOffset) + .clickAndHold() + .moveToElement(overlay, 0, dragOffset) + .release() + .perform(); + + WebDriverWrapper.waitFor(() -> !elementCache().yZoomButtons.findElements(plotEl).isEmpty(), + "Zoom buttons did not appear after y-axis drag", WAIT_FOR_JAVASCRIPT); + + WebElement buttonsGroup = elementCache().yZoomButtons.findElement(plotEl); + Locator.css("g").findElement(buttonsGroup).click(); + } + + public boolean isZoomActive(QCPlot qcPlot) + { + return !elementCache().yZoomBorder.findElements(qcPlot.getPlot()).isEmpty(); + } + + public boolean isResetZoomVisible(QCPlot qcPlot) + { + return !elementCache().yZoomResetLink.findElements(qcPlot.getPlot()).isEmpty(); + } + + public void clickResetZoom(QCPlot qcPlot) + { + elementCache().yZoomResetLink.findElement(qcPlot.getPlot()).click(); + } + public class Elements extends BodyWebPart.ElementCache { WebElement startDate = Locator.css("#start-date-field input").findWhenNeeded(this); @@ -939,6 +975,10 @@ public class Elements extends BodyWebPart.ElementCache WebElement plotPanel = Locator.css("div.tiledPlotPanel").findWhenNeeded(this); WebElement paginationPanel = Locator.css("div.plotPaginationHeaderPanel").findWhenNeeded(this); Locator extFormDisplay = Locator.css("div.x4-form-display-field"); + Locator.CssLocator yZoomOverlay = Locator.css("svg rect.y-zoom-overlay"); + Locator.CssLocator yZoomButtons = Locator.css("svg g.y-zoom-buttons"); + Locator.CssLocator yZoomBorder = Locator.css("svg rect.y-zoom-border"); + Locator.CssLocator yZoomResetLink = Locator.css("svg text.qc-reset-zoom-link"); Locator.CssLocator guideSetTrainingRect = Locator.css("svg rect.training"); Locator.CssLocator experimentRangeRect = Locator.css("svg rect.expRange"); Locator.CssLocator guideSetSvgButton = Locator.css("svg g.guideset-svg-button text"); diff --git a/test/src/org/labkey/test/tests/targetedms/TargetedMSQCTest.java b/test/src/org/labkey/test/tests/targetedms/TargetedMSQCTest.java index b51926618..5982cef4f 100644 --- a/test/src/org/labkey/test/tests/targetedms/TargetedMSQCTest.java +++ b/test/src/org/labkey/test/tests/targetedms/TargetedMSQCTest.java @@ -1148,6 +1148,61 @@ private void verifyRow(DataRegionTable drt, int row, String sampleName, String s assertEquals(skylineDocName, drt.getDataAsText(row, "File")); } + @Test + public void testQCPlotYAxisZoom() + { + PanoramaDashboard qcDashboard = new PanoramaDashboard(this); + QCPlotsWebPart qcPlotsWebPart = qcDashboard.getQcPlotsWebPart(); + qcPlotsWebPart.filterQCPlotsToInitialData(PRECURSORS.length, true); + + List plots = qcPlotsWebPart.getPlots(); + assertTrue("Expected at least 2 plots for y-axis zoom test", plots.size() >= 2); + + // 1. Verify zooming is possible: drag on y-axis, zoom buttons appear, zoom applies + log("Verifying y-axis zoom can be applied"); + qcPlotsWebPart.performYAxisZoom(plots.get(0)); + waitForElement(Locator.css("svg rect.y-zoom-border"), WAIT_FOR_JAVASCRIPT); + + plots = qcPlotsWebPart.getPlots(); + QCPlot firstPlot = plots.get(0); + QCPlot secondPlot = plots.get(1); + + assertTrue("Zoom border should appear on first plot after zoom", qcPlotsWebPart.isZoomActive(firstPlot)); + assertTrue("Reset Zoom link should appear on first plot after zoom", qcPlotsWebPart.isResetZoomVisible(firstPlot)); + + // 2. Verify zoom is per-plot: second plot is unaffected + log("Verifying zoom is independent per plot"); + assertFalse("Second plot should not be zoomed", qcPlotsWebPart.isZoomActive(secondPlot)); + assertFalse("Reset Zoom link should not appear on second plot", qcPlotsWebPart.isResetZoomVisible(secondPlot)); + + // 3. Verify reset works for the zoomed plot only + log("Verifying Reset Zoom removes zoom on the target plot"); + qcPlotsWebPart.clickResetZoom(firstPlot); + waitForElementToDisappear(Locator.css("svg rect.y-zoom-border"), WAIT_FOR_JAVASCRIPT); + + plots = qcPlotsWebPart.getPlots(); + firstPlot = plots.get(0); + + assertFalse("Zoom border should be gone after reset", qcPlotsWebPart.isZoomActive(firstPlot)); + assertFalse("Reset Zoom link should be gone after reset", qcPlotsWebPart.isResetZoomVisible(firstPlot)); + + // 4. Verify zoom is not persisted after page reload + log("Verifying zoom state is cleared on page reload"); + qcPlotsWebPart.performYAxisZoom(firstPlot); + waitForElement(Locator.css("svg rect.y-zoom-border"), WAIT_FOR_JAVASCRIPT); + + refresh(); + qcDashboard = new PanoramaDashboard(this); + qcPlotsWebPart = qcDashboard.getQcPlotsWebPart(); + qcPlotsWebPart.filterQCPlotsToInitialData(PRECURSORS.length, true); + + plots = qcPlotsWebPart.getPlots(); + firstPlot = plots.get(0); + + assertFalse("Zoom should not persist after page reload", qcPlotsWebPart.isZoomActive(firstPlot)); + assertFalse("Reset Zoom link should not appear after page reload", qcPlotsWebPart.isResetZoomVisible(firstPlot)); + } + private void createAndInsertAnnotations() { clickTab("Annotations");