diff --git a/resources/views/peptideSummary.html b/resources/views/peptideSummary.html index 19a97e173..5c889fb95 100644 --- a/resources/views/peptideSummary.html +++ b/resources/views/peptideSummary.html @@ -271,7 +271,8 @@ function applyHeatmapColors() { const table = document.getElementById('heatmap-table'); - const values = []; + let min = Infinity; + let max = 0; // Extract values from table cells, excluding the last row (totals row) for (let i = 1; i < table.rows.length - 1; i++) { // skip header @@ -279,13 +280,12 @@ for (let j = 1; j < row.cells.length - 1; j++) { // skip the first and last column const value = parseFloat(row.cells[j].textContent); if (!isNaN(value)) { - values.push(value); + min = Math.min(min, value); + max = Math.max(max, value); } } } - const min = Math.min(...values); - const max = Math.max(...values, 0); document.getElementById('max-outliers').innerHTML = ' ' + max + ' outliers'; // Apply colors based on values, excluding the last row (totals row) for (let i = 1; i < table.rows.length - 1; i++) { // skip header @@ -303,7 +303,7 @@ } function getColor(value, min, max) { - const percent = (value - min) / (max - min); + const percent = max > min ? (value - min) / (max - min) : 1; const red = 255; const green = Math.floor(255 * (1 - percent)); const blue = Math.floor(255 * (1 - percent)); diff --git a/src/org/labkey/targetedms/SkylineDocImporter.java b/src/org/labkey/targetedms/SkylineDocImporter.java index 2cafc1a2a..2c4405e49 100644 --- a/src/org/labkey/targetedms/SkylineDocImporter.java +++ b/src/org/labkey/targetedms/SkylineDocImporter.java @@ -261,7 +261,10 @@ public TargetedMSRun importRun(RunInfo runInfo, PipelineJob job) throws IOExcept TargetedMSManager.get().clearQCMetricCache(run.getContainer(), true); _progressMonitor.complete(); - return TargetedMSManager.getRun(_runId); + TargetedMSRun importedRun = TargetedMSManager.getRun(_runId); + if (importedRun == null) + throw new PipelineJobException("Could not find record for runId " + _runId + ", it may have already been deleted. Please try importing again."); + return importedRun; } catch (FileNotFoundException fnfe) { diff --git a/src/org/labkey/targetedms/TargetedMSController.java b/src/org/labkey/targetedms/TargetedMSController.java index a32b55fbc..cff8efc77 100644 --- a/src/org/labkey/targetedms/TargetedMSController.java +++ b/src/org/labkey/targetedms/TargetedMSController.java @@ -145,6 +145,7 @@ import org.labkey.api.security.permissions.InsertPermission; import org.labkey.api.security.permissions.ReadPermission; import org.labkey.api.security.permissions.UpdatePermission; +import org.labkey.api.sql.LabKeySql; import org.labkey.api.targetedms.RepresentativeDataState; import org.labkey.api.targetedms.RunRepresentativeDataState; import org.labkey.api.targetedms.TargetedMSService; @@ -3067,6 +3068,12 @@ public void setUpdate(boolean update) } public void appendReplicateFilters(SQLFragment sql, String colName) + { + appendReplicateFilters(sql, colName, false); + } + + /** @param labKeySql inline annotation values as literals, since LabKey SQL text can't carry parameters */ + public void appendReplicateFilters(SQLFragment sql, String colName, boolean labKeySql) { List replicateIds = getReplicatesFilterList(); if (!replicateIds.isEmpty()) @@ -3091,8 +3098,16 @@ public void appendReplicateFilters(SQLFragment sql, String colName) { if (!first) sql.append(" OR "); - sql.append(" (name = ").appendValue(annotation.getName()) - .append(" AND value = ").appendValue(annotation.getValue()).append(")"); + if (labKeySql) + { + sql.append(" (name = ").append(LabKeySql.quoteString(annotation.getName())) + .append(" AND value = ").append(LabKeySql.quoteString(annotation.getValue())).append(")"); + } + else + { + sql.append(" (name = ").appendValue(annotation.getName()) + .append(" AND value = ").appendValue(annotation.getValue()).append(")"); + } first = false; } sql.append(")"); diff --git a/src/org/labkey/targetedms/TargetedMSModule.java b/src/org/labkey/targetedms/TargetedMSModule.java index fd5f6ff36..c9e2425c6 100644 --- a/src/org/labkey/targetedms/TargetedMSModule.java +++ b/src/org/labkey/targetedms/TargetedMSModule.java @@ -66,6 +66,7 @@ import org.labkey.panoramapremium.PanoramaPremiumController; import org.labkey.panoramapremium.QCNotificationSender; import org.labkey.panoramapremium.View.QCSummaryMenuCustomizer; +import org.labkey.targetedms.chart.ChromatogramDataset; import org.labkey.targetedms.chart.ComparisonCategory; import org.labkey.targetedms.chart.ReplicateLabelMinimizer; import org.labkey.targetedms.datasource.MsDataSourceUtil; @@ -714,6 +715,7 @@ protected void startupAfterSpringConfig(ModuleContext moduleContext) public @NotNull Set> getUnitTests() { return Set.of( + ChromatogramDataset.TestCase.class, ComparisonCategory.TestCase.class, ReplicateLabelMinimizer.TestCase.class, SampleFile.TestCase.class, diff --git a/src/org/labkey/targetedms/chart/ChromatogramDataset.java b/src/org/labkey/targetedms/chart/ChromatogramDataset.java index 4f00f2372..c1a5248fa 100644 --- a/src/org/labkey/targetedms/chart/ChromatogramDataset.java +++ b/src/org/labkey/targetedms/chart/ChromatogramDataset.java @@ -21,6 +21,8 @@ import org.jfree.data.xy.XYDataItem; import org.jfree.data.xy.XYSeries; import org.jfree.data.xy.XYSeriesCollection; +import org.junit.Assert; +import org.junit.Test; import org.labkey.api.collections.IntHashMap; import org.labkey.api.data.Container; import org.labkey.api.data.DataRegionSelection; @@ -29,6 +31,7 @@ import org.labkey.api.security.User; import org.labkey.api.util.Formats; import org.labkey.api.util.logging.LogHelper; +import org.labkey.api.view.NotFoundException; import org.labkey.api.view.ViewContext; import org.labkey.targetedms.TargetedMSManager; import org.labkey.targetedms.TargetedMSRun; @@ -114,6 +117,15 @@ public ChromatogramDataset(User u, Container c) _user = u; } + /** JFreeChart rejects duplicate series keys, so suffix repeats as "X (2)", "X (3)", ... */ + static String uniqueSeriesKey(XYSeriesCollection dataset, String label) + { + String key = label; + for (int suffix = 2; dataset.getSeriesIndex(key) >= 0; suffix++) + key = label + " (" + suffix + ")"; + return key; + } + public abstract String getChartTitle(); // Start of the peak integration boundary. Shown as a vertical dotted line @@ -322,7 +334,11 @@ public SampleFileDataset(SampleFileChromInfo chromInfo, User user, Container con super(user, container); _chromInfo = chromInfo; SampleFile sampleFile = TargetedMSManager.getSampleFile(chromInfo.getSampleFileId(), container); + if (sampleFile == null) + throw new NotFoundException("Could not find sample file " + chromInfo.getSampleFileId() + " in " + container.getPath()); Replicate replicate = TargetedMSManager.getReplicate(sampleFile.getReplicateId(), container); + if (replicate == null) + throw new NotFoundException("Could not find replicate " + sampleFile.getReplicateId() + " for sample file " + sampleFile.getId() + " in " + container.getPath()); _run = TargetedMSManager.getRun(replicate.getRunId()); } @@ -645,7 +661,8 @@ public void build() Chromatogram chromatogram = pChromInfo.createChromatogram(_run); if (chromatogram != null) { - XYSeries series = new XYSeries(getLabel(pChromInfo)); + // Molecule labels omit isotope label and adduct, so two precursors can share one + XYSeries series = new XYSeries(uniqueSeriesKey(_jfreeDataset, getLabel(pChromInfo))); _jfreeDataset.addSeries(series); // Instead of displaying separate peaks for each transition of this precursor, // we will sum up the intensities and display a single peak for the precursor @@ -1081,7 +1098,7 @@ GeneralMoleculeDataset.PeakInChart addTransitionAsSeries(XYSeriesCollection data float[] intensities = chromatogram.getIntensities(tci.getChromatogramIndex()); assert times.length == intensities.length : "Length of times and intensities don't match"; - XYSeries series = new XYSeries(label); + XYSeries series = new XYSeries(uniqueSeriesKey(dataset, label)); double maxTraceIntensity = 0; // maximum intensity in the displayed range. @@ -1224,15 +1241,17 @@ protected void buildJFreedataset(Chromatogram chromatogram, RtRange chromatogram List transitions = MoleculeTransitionManager.getTransitionsForPrecursor(_molPrecursor.getId(), _user, _container); if (transitions.size() != _pChromInfo.getTransitionChromatogramIndicesList().size()) { - throw new IllegalStateException("Mismatch in transitions and indices lengths: " + transitions.size() + " vs " + _pChromInfo.getTransitionChromatogramIndicesList().size()); + LOG.warn("Precursor {} has {} transitions but {} chromatogram indices; trace labels may be mismatched", + _molPrecursor.getId(), transitions.size(), _pChromInfo.getTransitionChromatogramIndicesList().size()); } int index = 0; for (MoleculeTransition transition : transitions) { - if (include(transition)) + if (include(transition) && index < _pChromInfo.getTransitionChromatogramIndicesList().size()) { - tciList.add(new MoleculeTransChromInfoPlusTransition(_pChromInfo.makeDummyTransitionChromInfo(index++), transition)); + tciList.add(new MoleculeTransChromInfoPlusTransition(_pChromInfo.makeDummyTransitionChromInfo(index), transition)); } + index++; } } @@ -1629,14 +1648,7 @@ public void build() GeneralMolecule gm = entry.getKey(); // Issue 48258:Some very old docs have duplicate molecule names - String seriesName = gm.getTextId(); - int suffix = 1; - while (hasSeries(_jfreeDataset, seriesName)) - { - seriesName = gm.getTextId() + " " + (++suffix); - } - - XYSeries series = new XYSeries(seriesName, true, false); + XYSeries series = new XYSeries(uniqueSeriesKey(_jfreeDataset, gm.getTextId()), true, false); _jfreeDataset.addSeries(series); _seriesColors.put(i, ColorGenerator.getColor(gm.getTextId(), _seriesColors.values())); @@ -1674,18 +1686,6 @@ public void build() _maxDisplayRt = fullRange.getMaxRt() + padding; } - private boolean hasSeries(XYSeriesCollection jfreeDataset, String seriesName) - { - for (XYSeries series : (List)jfreeDataset.getSeries()) - { - if (seriesName.equals(series.getKey())) - { - return true; - } - } - return false; - } - private RtRange getChromatogramRange(GeneralMolecule generalMolecule, List chromInfos) { List ranges = new ArrayList<>(); @@ -1715,4 +1715,26 @@ private RtRange getChromatogramRange(GeneralMolecule generalMolecule, List return new RtRange(summary._minRt, summary._maxRt, true, _syncRt); } } + + public static class TestCase extends Assert + { + @Test + public void testUniqueSeriesKey() + { + XYSeriesCollection dataset = new XYSeriesCollection(); + assertEquals("A", uniqueSeriesKey(dataset, "A")); + + dataset.addSeries(new XYSeries("A")); + assertEquals("A (2)", uniqueSeriesKey(dataset, "A")); + assertEquals("B", uniqueSeriesKey(dataset, "B")); + + dataset.addSeries(new XYSeries("A (2)")); + assertEquals("A (3)", uniqueSeriesKey(dataset, "A")); + + // A real label that looks like a suffixed duplicate must not collide either + dataset.addSeries(new XYSeries("A (3)")); + assertEquals("A (2) (2)", uniqueSeriesKey(dataset, "A (2)")); + assertEquals("A (4)", uniqueSeriesKey(dataset, "A")); + } + } } diff --git a/src/org/labkey/targetedms/outliers/OutlierGenerator.java b/src/org/labkey/targetedms/outliers/OutlierGenerator.java index 94ea18405..ff2ae05b8 100644 --- a/src/org/labkey/targetedms/outliers/OutlierGenerator.java +++ b/src/org/labkey/targetedms/outliers/OutlierGenerator.java @@ -199,6 +199,8 @@ public void cachePrecursorMetricValues(TargetedMSSchema schema, List getRawMetricDataSets(TargetedMSSchema schema, List configurations, Date startDate, Date endDate, List annotationGroups, boolean showExcluded, boolean showExcludedPrecursors) { List result = new ArrayList<>(); + if (configurations.isEmpty()) + return result; TableInfo sampleFileForQC = schema.getTable("SampleFileForQC"); List sfs = new TableSelector(sampleFileForQC).getArrayList(SampleFileQCMetadata.class); diff --git a/src/org/labkey/targetedms/query/GeneralMoleculePrecursorChromatogramsTableInfo.java b/src/org/labkey/targetedms/query/GeneralMoleculePrecursorChromatogramsTableInfo.java index 4d735fb8d..8103a6a75 100644 --- a/src/org/labkey/targetedms/query/GeneralMoleculePrecursorChromatogramsTableInfo.java +++ b/src/org/labkey/targetedms/query/GeneralMoleculePrecursorChromatogramsTableInfo.java @@ -154,7 +154,7 @@ private static TableInfo getPivotByPrecursorChromInfoTable(Container container, sql.append(" WHERE ").append(precursorIdKey).append(".GeneralMoleculeId=").appendValue(generalMolecule.getId()); sql.append(" AND OptimizationStep IS NULL "); // Ignore precursorChromInfos for optimization peaks (e.g. Collision energy optimization) - form.appendReplicateFilters(sql, "SampleFileId.ReplicateId"); + form.appendReplicateFilters(sql, "SampleFileId.ReplicateId", true); sql.append(" ) X"); sql.append("\n GROUP BY replicate, sample, ").append(_generalMoleculeChromInfoCol).append(", isotopecharge") diff --git a/test/src/org/labkey/test/tests/targetedms/TargetedMSMultiplePeptidePlotTest.java b/test/src/org/labkey/test/tests/targetedms/TargetedMSMultiplePeptidePlotTest.java index 878fca294..582075e91 100644 --- a/test/src/org/labkey/test/tests/targetedms/TargetedMSMultiplePeptidePlotTest.java +++ b/test/src/org/labkey/test/tests/targetedms/TargetedMSMultiplePeptidePlotTest.java @@ -27,13 +27,14 @@ import org.openqa.selenium.support.ui.ExpectedConditions; import java.io.File; +import java.util.Arrays; import java.util.List; /** * Tests rendering of multiple peptides on the same chromatogram visualization panel. */ @Category({}) -@BaseWebDriverTest.ClassTimeout(minutes = 2) +@BaseWebDriverTest.ClassTimeout(minutes = 4) public class TargetedMSMultiplePeptidePlotTest extends TargetedMSTest { @BeforeClass @@ -119,4 +120,49 @@ public void testMultiplePeptidePlotOnSameChromatogram() // Each row will have chromatograms from two replicates. We expect 5 rows since we are displaying the first 10 replicates. checker().verifyEquals("Incorrect row count after changing row size to 2 replicates per row", 5, table.getDataRowCount()); } + + @Test + public void testReplicateAnnotationFilter() + { + // Experiment folder, since importing a second SProCoP doc into the Library folder would create conflicts + String subfolder = "AnnotationFilter"; + setupSubfolder(getProjectName(), subfolder, FolderType.Experiment); + importData(SProCoP_FILE_ANNOTATED, 1, false, false); // Six replicates; DB maintenance would only cost time + + navigateToFolder(getProjectName(), subfolder); + clickAndWait(Locator.linkWithText(SProCoP_FILE_ANNOTATED)); + clickAndWait(Locator.linkWithText("gi|14278146|pdb|1B8E|")); + clickAndWait(Locator.linkWithText("VYVEELKPTPEGDLEILLQK")); + verifyFilteredReplicates(List.of("JGB_02", "JGB_32", "JGB_38", "JGB_73", "JGB_87", "JGB_87_130815040004")); + + applyAnnotationFilter("Day : 1"); + verifyFilteredReplicates(List.of("JGB_02", "JGB_32", "JGB_38")); + checker().screenShotIfNewError("singleAnnotationFilter"); + + applyAnnotationFilter("SampleIdentifier : AnnotatedSample1", "Day : 2"); + verifyFilteredReplicates(List.of("JGB_02", "JGB_73", "JGB_87", "JGB_87_130815040004")); + checker().screenShotIfNewError("multipleAnnotationFilters"); + } + + private void applyAnnotationFilter(String... annotations) + { + log("Filtering chromatograms by replicate annotations: " + Arrays.toString(annotations)); + click(Locator.tagWithText("strong", "Display Chart Settings")); + new ComboBox.ComboBoxFinder(getDriver()).withInputNamed("annotationsFilter") + .findWhenNeeded(getDriver()).setMultiSelect(true).selectComboBoxItem(annotations); + clickButton("Update"); + } + + private void verifyFilteredReplicates(List expectedReplicates) + { + DataRegionTable table = new DataRegionTable.DataRegionFinder(getDriver()).withName("PeptidePrecursorChromatograms").waitFor(); + waitForElementToDisappear(Locator.tagWithAttributeContaining("div", "alt", "Chromatogram Q_Exactive") + .withText("Loading..."), WebDriverWrapper.WAIT_FOR_PAGE); + checker().verifyEquals("Incorrect number of replicates after annotation filter", expectedReplicates.size(), table.getDataRowCount()); + for (String replicate : expectedReplicates) + { + checker().verifyTrue("Missing chromatogram for replicate " + replicate, + isElementPresent(Locator.tagWithAttributeContaining("div", "alt", "Chromatogram Q_Exactive_08_09_2013_" + replicate))); + } + } } diff --git a/webapp/TargetedMS/js/BaseQCPlotPanel.js b/webapp/TargetedMS/js/BaseQCPlotPanel.js index 272480eb0..04630db1b 100644 --- a/webapp/TargetedMS/js/BaseQCPlotPanel.js +++ b/webapp/TargetedMS/js/BaseQCPlotPanel.js @@ -19,6 +19,10 @@ Ext4.define('LABKEY.targetedms.BaseQCPlotPanel', { return undefined; }, + isPrecursorScopedMetric: function(id) { + return !!this.getMetricPropsById(id)?.precursorScoped; + }, + isMultiSeries : function() { return this.metric && this.metric2; }, diff --git a/webapp/TargetedMS/js/QCPlotHelperBase.js b/webapp/TargetedMS/js/QCPlotHelperBase.js index 1f2593187..4c07cd341 100644 --- a/webapp/TargetedMS/js/QCPlotHelperBase.js +++ b/webapp/TargetedMS/js/QCPlotHelperBase.js @@ -213,6 +213,10 @@ Ext4.define("LABKEY.targetedms.QCPlotHelperBase", { let trainingSeqIdx = 1; // this index is used for displaying the average number of runs in tooltip (QCPlotHoverPanel.js L110) for (let i = this.pagingStartIndex; i < this.pagingEndIndex; i++) { const plotDataRow = plotDataRows[i]; + if (!plotDataRow) { + console.warn('No QC plot data row at index ' + i + ' of ' + plotDataRows.length); + continue; + } tempData = plotDataRow; const fragment = plotDataRow.SeriesLabel; Ext4.iterate(plotDataRow.data, function (plotData) { @@ -481,7 +485,7 @@ Ext4.define("LABKEY.targetedms.QCPlotHelperBase", { metricProps[this.metric2] = this.getMetricPropsById(this.metric2); } - if (this.singlePlot && this.getMetricPropsById(this.metric).precursorScoped) { + if (this.singlePlot && this.isPrecursorScopedMetric(this.metric)) { this.peptideGroups = this.buildPeptideGroups(); addedPlot = this.addCombinedPeptideSinglePlot(metricProps); } @@ -866,7 +870,7 @@ Ext4.define("LABKEY.targetedms.QCPlotHelperBase", { getSubtitle: function(precursor) { if (!this.isMultiSeries()) { - return (precursor ? (precursor + ' - ') : '') + this.getMetricPropsById(this.metric).name; + return (precursor ? (precursor + ' - ') : '') + (this.getMetricPropsById(this.metric)?.name ?? ''); } return precursor; }, @@ -1202,7 +1206,7 @@ Ext4.define("LABKEY.targetedms.QCPlotHelperBase", { } const extraMargin = this.showInPlotLegends() ? 0 : 10 * this.longestLegendText; - this.attachPlotExportIcons(id, mainTitle + '-' + this.precursors[precursorIndex] + '-' + this.getMetricPropsById(this.metric).name, plotIndex, this.getPlotWidth(), extraMargin); + this.attachPlotExportIcons(id, mainTitle + '-' + this.precursors[precursorIndex] + '-' + (this.getMetricPropsById(this.metric)?.name ?? ''), plotIndex, this.getPlotWidth(), extraMargin); }, getYAxisLeftMarginOffset: function(precursorInfo) { diff --git a/webapp/TargetedMS/js/QCSummaryPanel.js b/webapp/TargetedMS/js/QCSummaryPanel.js index 5e622be02..08a72f1ce 100644 --- a/webapp/TargetedMS/js/QCSummaryPanel.js +++ b/webapp/TargetedMS/js/QCSummaryPanel.js @@ -305,7 +305,7 @@ Ext4.define('LABKEY.targetedms.QCSummary', { }, queryContainerSampleFileStats: function (container) { - if (container.fileCount > 0) { + if (container.fileCount > 0 && container.docCount > 0) { LABKEY.Ajax.request({ url: LABKEY.ActionURL.buildURL('targetedms', 'GetQCMetricOutliers.api', container.path), params: {sampleLimit: this.sampleLimit}, @@ -323,6 +323,9 @@ Ext4.define('LABKEY.targetedms.QCSummary', { }, failure: LABKEY.Utils.getCallbackWrapper(function(response) { var sampleFilesDiv = Ext4.get('qc-summary-samplefiles-' + container.id); + if (!sampleFilesDiv) { + return; + } if (response.message) { sampleFilesDiv.update("" + Ext4.util.Format.htmlEncode(response.message) + ""); @@ -342,6 +345,9 @@ Ext4.define('LABKEY.targetedms.QCSummary', { removeSampleFilesDetailsDiv: function (container) { var sampleFilesDiv = Ext4.get('qc-summary-samplefiles-' + container.id); + if (!sampleFilesDiv) { + return; + } sampleFilesDiv.update(''); sampleFilesDiv.removeCls('sample-file-details-loading'); }, @@ -426,6 +432,9 @@ Ext4.define('LABKEY.targetedms.QCSummary', { html += ''; } var sampleFilesDiv = Ext4.get('qc-summary-samplefiles-' + container.id); + if (!sampleFilesDiv) { + return; + } sampleFilesDiv.update(html); sampleFilesDiv.removeCls('sample-file-details-loading'); diff --git a/webapp/TargetedMS/js/QCTrendPlotPanel.js b/webapp/TargetedMS/js/QCTrendPlotPanel.js index e0e59077f..7f0905d9c 100644 --- a/webapp/TargetedMS/js/QCTrendPlotPanel.js +++ b/webapp/TargetedMS/js/QCTrendPlotPanel.js @@ -329,7 +329,7 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { if (this.metric) { //hiding the show All series in a single plot checkbox for run scoped metrics - this.getPlotGroupRadioGroup().setVisible(this.getMetricPropsById(this.metric).precursorScoped); + this.getPlotGroupRadioGroup().setVisible(this.isPrecursorScopedMetric(this.metric)); } // Create vertical line separator component @@ -376,8 +376,7 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { // Ensure single-plot checkbox visibility mirrors previous logic when metrics are run-scoped if (this.metric) { - var showSinglePlot = this.getMetricPropsById(this.metric).precursorScoped; - this.getPlotGroupRadioGroup().setVisible(showSinglePlot); + this.getPlotGroupRadioGroup().setVisible(this.isPrecursorScopedMetric(this.metric)); } const columnsToolbar = Ext4.create('Ext.toolbar.Toolbar', { @@ -462,7 +461,7 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { if (this.trailingRuns === undefined || this.trailingRuns === null) { this.trailingRuns = 10; } - this.getTrailingRunsField().setVisible(newValues.indexOf("Trailing Mean") > -1 || newValues.indexOf("Trailing CV") > -1); + this.getTrailingRunsField().setVisible(this.plotTypes.indexOf("Trailing Mean") > -1 || this.plotTypes.indexOf("Trailing CV") > -1); this.havePlotOptionsChanged = true; this.displayTrendPlot(); } @@ -936,8 +935,7 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { } this.havePlotOptionsChanged = true; // Update single-plot checkbox visibility directly in the 3-column layout - var showAllSeriesCheckBox = this.getMetricPropsById(this.metric).precursorScoped; - this.getPlotGroupRadioGroup().setVisible(showAllSeriesCheckBox); + this.getPlotGroupRadioGroup().setVisible(this.isPrecursorScopedMetric(this.metric)); if (this.filterQCPoints) { this.resetFilterPointsIndices(); @@ -1347,7 +1345,7 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { if (this.pagingStartIndex < 0) this.pagingStartIndex = 0; else if (this.pagingStartIndex > plotDataRowsBySeriesLabel.length) - this.pagingStartIndex = plotDataRowsBySeriesLabel.length - this.maxCount; + this.pagingStartIndex = Math.max(0, plotDataRowsBySeriesLabel.length - this.maxCount); this.pagingEndIndex = Math.min(this.pagingStartIndex + this.maxCount, plotDataRowsBySeriesLabel.length); @@ -1655,6 +1653,7 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { }); let observer = null; + let showTimeout = null; tippy(point, { allowHTML: true, @@ -1685,6 +1684,9 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { }, onShow(instance) { observer = new MutationObserver((mutations) => { + if (!instance.state.isVisible || !instance.popperInstance) { + return; + } const hoverPanel = container.querySelector('.qc-plot-hover-panel'); if (hoverPanel) { hoverPanel.style.minWidth = 'auto'; @@ -1700,7 +1702,11 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { }); observer.observe(container, { childList: true, subtree: true }); - setTimeout(() => { + showTimeout = setTimeout(() => { + showTimeout = null; + if (!instance.state.isVisible || !instance.popperInstance) { + return; + } const hoverPanel = container.querySelector('.qc-plot-hover-panel'); if (hoverPanel && instance.popper.style.visibility === 'hidden') { hoverPanel.style.minWidth = 'auto'; @@ -1717,6 +1723,10 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { observer.disconnect(); observer = null; } + if (showTimeout) { + clearTimeout(showTimeout); + showTimeout = null; + } } }); } else {