From 7befff40808f9ba9b3085021232371d760778d17 Mon Sep 17 00:00:00 2001 From: ankurjuneja Date: Tue, 14 Apr 2026 16:55:41 -0700 Subject: [PATCH 1/5] sort precursor in combined qc plots legend by precursor row ID --- src/org/labkey/targetedms/model/QCPlotFragment.java | 13 +++++++++++++ .../targetedms/outliers/OutlierGenerator.java | 11 +++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/org/labkey/targetedms/model/QCPlotFragment.java b/src/org/labkey/targetedms/model/QCPlotFragment.java index f03f47b8c..8fd2c5705 100644 --- a/src/org/labkey/targetedms/model/QCPlotFragment.java +++ b/src/org/labkey/targetedms/model/QCPlotFragment.java @@ -21,6 +21,8 @@ public class QCPlotFragment private List guideSetStats; @Nullable private Color _seriesColor; + @Nullable + private Long precursorRowId; public String getSeriesLabel() { @@ -205,4 +207,15 @@ public Color getSeriesColor() { return _seriesColor; } + + @Nullable + public Long getPrecursorRowId() + { + return precursorRowId; + } + + public void setPrecursorRowId(Long precursorRowId) + { + this.precursorRowId = precursorRowId; + } } diff --git a/src/org/labkey/targetedms/outliers/OutlierGenerator.java b/src/org/labkey/targetedms/outliers/OutlierGenerator.java index e133136ea..7a0061f06 100644 --- a/src/org/labkey/targetedms/outliers/OutlierGenerator.java +++ b/src/org/labkey/targetedms/outliers/OutlierGenerator.java @@ -539,7 +539,11 @@ public List getQCPlotFragment(List rawMetricDa Optional bestPrecursorIdRow = entry.getValue().stream().filter(x -> x.getPrecursorId() != null).min(Comparator.comparing(RawMetricDataSet::getPrecursorId)); // Remember the precursor ID so that we can assign a series color based on Skyline's algorithm - bestPrecursorIdRow.ifPresent(rawMetricDataSet -> fragmentsByPrecursorId.put(rawMetricDataSet.getPrecursorId(), qcPlotFragment)); + // and to sort by Skyline document order (row ID) instead of alphabetically + bestPrecursorIdRow.ifPresent(rawMetricDataSet -> { + fragmentsByPrecursorId.put(rawMetricDataSet.getPrecursorId(), qcPlotFragment); + qcPlotFragment.setPrecursorRowId(rawMetricDataSet.getPrecursorId()); + }); qcPlotFragment.setSeriesLabel(entry.getKey()); qcPlotFragment.setQcPlotData(entry.getValue()); @@ -591,7 +595,10 @@ public List getQCPlotFragment(List rawMetricDa } } - qcPlotFragments.sort(Comparator.comparing(QCPlotFragment::getSeriesLabel)); + // Sort by precursor row ID to preserve Skyline document order. Fragments with no precursor ID + // (e.g. trace metrics) fall back to alphabetical order after all precursor-scoped series. + qcPlotFragments.sort(Comparator.comparing(QCPlotFragment::getPrecursorRowId, Comparator.nullsLast(Comparator.naturalOrder())) + .thenComparing(QCPlotFragment::getSeriesLabel)); return qcPlotFragments; } From 2ca7e4fffc754383836f115f6fefc3b205fdd5b1 Mon Sep 17 00:00:00 2001 From: ankurjuneja Date: Tue, 14 Apr 2026 17:43:33 -0700 Subject: [PATCH 2/5] Let users toggle a precursor's series by clicking on its color swatch --- webapp/TargetedMS/js/QCPlotHelperBase.js | 2 + webapp/TargetedMS/js/QCTrendPlotPanel.js | 58 ++++++++++++++++++++++-- 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/webapp/TargetedMS/js/QCPlotHelperBase.js b/webapp/TargetedMS/js/QCPlotHelperBase.js index c4b7c7dc3..a533ee9e3 100644 --- a/webapp/TargetedMS/js/QCPlotHelperBase.js +++ b/webapp/TargetedMS/js/QCPlotHelperBase.js @@ -822,6 +822,8 @@ Ext4.define("LABKEY.targetedms.QCPlotHelperBase", { const plot = LABKEY.vis.TrendingLinePlot(plotConfig); plot.render(); + this.attachCombinedLegendClickHandlers(); + this.addAnnotationsToPlot(plot, combinePlotData); this.addGuideSetTrainingRangeToPlot(plot, combinePlotData); diff --git a/webapp/TargetedMS/js/QCTrendPlotPanel.js b/webapp/TargetedMS/js/QCTrendPlotPanel.js index 529153071..c06bf3837 100644 --- a/webapp/TargetedMS/js/QCTrendPlotPanel.js +++ b/webapp/TargetedMS/js/QCTrendPlotPanel.js @@ -77,6 +77,7 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { enableBrushing: false, havePlotOptionsChanged: false, selectedAnnotations: {}, + hiddenPrecursorSeries: null, // Plain object mapping fragment label -> true when hidden in the combined plot runs: null, trailingRuns: null, minWidth: 1250, // Keep in sync with the width defined in qcTrendPlot.jsp @@ -1612,12 +1613,17 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { }, plotPointMouseOut : function(event, row, layerSel, valueName, plotConfig) { + let hidden = this.hiddenPrecursorSeries || {}; d3.selectAll('.point path').attr('fill-opacity', 1).attr('stroke-opacity', 1); d3.selectAll('path.line').attr('fill-opacity', 1).attr('stroke-opacity', 1); - d3.selectAll('.legend .legend-item').attr('fill-opacity', 1).attr('stroke-opacity', 1); + d3.selectAll('.legend .legend-item').each(function(d) { + var opacity = (d && d.name && !d.separator && hidden[d.hoverText || d.name.split('|')[0]]) ? 0.3 : 1; + d3.select(this).attr('fill-opacity', opacity).attr('stroke-opacity', opacity); + }); }, highlightFragmentSeries : function(fragment) { + let hidden = this.hiddenPrecursorSeries || {}; var points = d3.selectAll('.point path'); var pointOpacityAcc = function(d) { return d.fragment === undefined || d.fragment === null || d.fragment === fragment ? 1 : 0.1 }; points.attr('fill-opacity', pointOpacityAcc).attr('stroke-opacity', pointOpacityAcc); @@ -1627,14 +1633,58 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { var lineOpacityAcc = function(d) { return d.group === undefined || d.group === null || d.group.indexOf(fragment + (hasYRightMetric ? '|' : '')) === 0 ? 1 : 0.1 }; lines.attr('fill-opacity', lineOpacityAcc).attr('stroke-opacity', lineOpacityAcc); - var legendItems = d3.selectAll('.legend .legend-item'); - var legendOpacityAcc = function(d) { + let legendItems = d3.selectAll('.legend .legend-item'); + let legendOpacityAcc = function(d) { if (!d.name) return 1; - return d.name.indexOf(fragment + (hasYRightMetric ? '|' : '')) === 0 ? 1 : 0.1 + let frag = d.hoverText || d.name.split('|')[0]; + if (hidden[frag]) return 0.3; // keep hidden series dimmed during hover + return d.name.indexOf(fragment + (hasYRightMetric ? '|' : '')) === 0 ? 1 : 0.1; }; legendItems.attr('fill-opacity', legendOpacityAcc).attr('stroke-opacity', legendOpacityAcc); }, + // Let users toggle a precursor's series by clicking on its color swatch. + toggleCombinedSeriesVisibility: function(fragment) { + if (!this.hiddenPrecursorSeries) { + this.hiddenPrecursorSeries = {}; + } + this.hiddenPrecursorSeries[fragment] = !this.hiddenPrecursorSeries[fragment]; + this.applySeriesVisibility(); + }, + + applySeriesVisibility: function() { + let hidden = this.hiddenPrecursorSeries || {}; + + d3.selectAll('.point path').attr('display', function(d) { + return (d && d.fragment && hidden[d.fragment]) ? 'none' : null; + }); + + d3.selectAll('path.line').attr('display', function(d) { + if (!d || !d.group) return null; + return hidden[d.group.split('|')[0]] ? 'none' : null; + }); + + d3.selectAll('.legend .legend-item').each(function(d) { + if (!d || !d.name || d.separator) return; + var opacity = hidden[d.hoverText || d.name.split('|')[0]] ? 0.3 : 1; + d3.select(this).attr('fill-opacity', opacity).attr('stroke-opacity', opacity); + }); + }, + + attachCombinedLegendClickHandlers: function() { + let me = this; + d3.selectAll('.legend .legend-item').each(function(d) { + if (!d || !d.name || d.separator) return; + d3.select(this) + .style('cursor', 'pointer') + .on('click.toggleSeries', function(d) { + d3.event.stopPropagation(); + me.toggleCombinedSeriesVisibility(d.hoverText || d.name.split('|')[0]); + }); + }); + this.applySeriesVisibility(); + }, + plotBrushStartEvent : function(plot) { this.clearPlotBrush(plot); }, From 4998b336d3416a64043da8b56ac75e519d2b90f1 Mon Sep 17 00:00:00 2001 From: ankurjuneja Date: Wed, 15 Apr 2026 09:24:44 -0700 Subject: [PATCH 3/5] Switch the legend to a tree that shows the protein/molecule list as a heading --- .../targetedms/TargetedMSController.java | 13 ++ .../targetedms/model/QCPlotFragment.java | 31 ++++ .../targetedms/outliers/OutlierGenerator.java | 10 ++ webapp/TargetedMS/css/qcTrendPlotReport.css | 14 ++ webapp/TargetedMS/js/QCPlotHelperBase.js | 2 + webapp/TargetedMS/js/QCPlotHelperWrapper.js | 11 ++ webapp/TargetedMS/js/QCTrendPlotPanel.js | 159 +++++++++++++++++- 7 files changed, 239 insertions(+), 1 deletion(-) diff --git a/src/org/labkey/targetedms/TargetedMSController.java b/src/org/labkey/targetedms/TargetedMSController.java index 952c0285c..6acf8c302 100644 --- a/src/org/labkey/targetedms/TargetedMSController.java +++ b/src/org/labkey/targetedms/TargetedMSController.java @@ -874,6 +874,7 @@ public static class LeveyJenningsPlotOptions private Integer _trailingRuns; private Integer _calendarMonthsToShow; private String _heatmapDataSource; + private String _hiddenSeries; public Map getAsMapOfStrings() { @@ -906,6 +907,8 @@ public Map getAsMapOfStrings() valueMap.put("calendarMonthsToShow", Integer.toString(_calendarMonthsToShow)); if (_heatmapDataSource != null) valueMap.put("heatMapDataSource", _heatmapDataSource); + if (_hiddenSeries != null) + valueMap.put("hiddenSeries", _hiddenSeries); // note: start and end date handled separately since they can be null and we want to persist that return valueMap; } @@ -1034,6 +1037,16 @@ public void setHeatmapDataSource(String heatmapDataSource) { _heatmapDataSource = heatmapDataSource; } + + public String getHiddenSeries() + { + return _hiddenSeries; + } + + public void setHiddenSeries(String hiddenSeries) + { + _hiddenSeries = hiddenSeries; + } } @RequiresPermission(ReadPermission.class) diff --git a/src/org/labkey/targetedms/model/QCPlotFragment.java b/src/org/labkey/targetedms/model/QCPlotFragment.java index 8fd2c5705..176ecb8a7 100644 --- a/src/org/labkey/targetedms/model/QCPlotFragment.java +++ b/src/org/labkey/targetedms/model/QCPlotFragment.java @@ -23,6 +23,10 @@ public class QCPlotFragment private Color _seriesColor; @Nullable private Long precursorRowId; + @Nullable + private Long peptideGroupId; + @Nullable + private String peptideGroupLabel; public String getSeriesLabel() { @@ -79,6 +83,11 @@ public JSONObject toJSON(boolean includeLJ, boolean includeMR, boolean includeMe JSONObject jsonObject = new JSONObject(); jsonObject.put("DataType", getDataType()); jsonObject.put("SeriesLabel", getSeriesLabel()); + if (peptideGroupId != null) + { + jsonObject.put("PeptideGroupId", peptideGroupId); + jsonObject.put("PeptideGroupLabel", peptideGroupLabel != null ? peptideGroupLabel : ""); + } if (_seriesColor != null) { jsonObject.put("SeriesColor", "#" + Integer.toHexString(_seriesColor.getRGB()).substring(2).toUpperCase()); @@ -218,4 +227,26 @@ public void setPrecursorRowId(Long precursorRowId) { this.precursorRowId = precursorRowId; } + + @Nullable + public Long getPeptideGroupId() + { + return peptideGroupId; + } + + public void setPeptideGroupId(Long peptideGroupId) + { + this.peptideGroupId = peptideGroupId; + } + + @Nullable + public String getPeptideGroupLabel() + { + return peptideGroupLabel; + } + + public void setPeptideGroupLabel(String peptideGroupLabel) + { + this.peptideGroupLabel = peptideGroupLabel; + } } diff --git a/src/org/labkey/targetedms/outliers/OutlierGenerator.java b/src/org/labkey/targetedms/outliers/OutlierGenerator.java index 7a0061f06..89d1e6ed1 100644 --- a/src/org/labkey/targetedms/outliers/OutlierGenerator.java +++ b/src/org/labkey/targetedms/outliers/OutlierGenerator.java @@ -44,9 +44,11 @@ import org.labkey.targetedms.model.SampleFileQCMetadata; import org.labkey.targetedms.parser.GeneralMolecule; import org.labkey.targetedms.parser.GeneralPrecursor; +import org.labkey.targetedms.parser.PeptideGroup; import org.labkey.targetedms.parser.SampleFile; import org.labkey.targetedms.query.MoleculeManager; import org.labkey.targetedms.query.MoleculePrecursorManager; +import org.labkey.targetedms.query.PeptideGroupManager; import org.labkey.targetedms.query.PeptideManager; import org.labkey.targetedms.query.PrecursorManager; @@ -592,6 +594,14 @@ public List getQCPlotFragment(List rawMetricDa Color color = ColorGenerator.getColor(molecule.getTextId(), seriesColors); entry.getValue().setSeriesColor(color); seriesColors.add(color); + + // set the peptide group (protein / molecule list) for the combined plot tree legend + PeptideGroup peptideGroup = PeptideGroupManager.getPeptideGroup(c, molecule.getPeptideGroupId()); + if (peptideGroup != null) + { + entry.getValue().setPeptideGroupId(peptideGroup.getId()); + entry.getValue().setPeptideGroupLabel(peptideGroup.getLabel()); + } } } diff --git a/webapp/TargetedMS/css/qcTrendPlotReport.css b/webapp/TargetedMS/css/qcTrendPlotReport.css index 80aab9dfd..70b1bd527 100644 --- a/webapp/TargetedMS/css/qcTrendPlotReport.css +++ b/webapp/TargetedMS/css/qcTrendPlotReport.css @@ -102,4 +102,18 @@ .x4-panel-header-text-container-default { font-weight: normal; +} + +/* Combined plot tree legend (shown when document has multiple proteins / molecule lists) */ +.qc-combined-tree-legend { + background: white; + line-height: 1.4; +} + +.qc-combined-tree-legend .qc-tree-group label:hover { + background-color: #f0f0f0; +} + +.qc-combined-tree-legend .qc-tree-precursor:hover { + background-color: #f0f0f0; } \ No newline at end of file diff --git a/webapp/TargetedMS/js/QCPlotHelperBase.js b/webapp/TargetedMS/js/QCPlotHelperBase.js index a533ee9e3..b78d6aa17 100644 --- a/webapp/TargetedMS/js/QCPlotHelperBase.js +++ b/webapp/TargetedMS/js/QCPlotHelperBase.js @@ -412,9 +412,11 @@ Ext4.define("LABKEY.targetedms.QCPlotHelperBase", { } if (this.singlePlot && this.getMetricPropsById(this.metric).precursorScoped) { + this.peptideGroups = this.buildPeptideGroups(); addedPlot = this.addCombinedPeptideSinglePlot(metricProps); } else { + this.peptideGroups = null; addedPlot = this.addIndividualPrecursorPlots(metricProps); } diff --git a/webapp/TargetedMS/js/QCPlotHelperWrapper.js b/webapp/TargetedMS/js/QCPlotHelperWrapper.js index 686efef74..25e3a0bca 100644 --- a/webapp/TargetedMS/js/QCPlotHelperWrapper.js +++ b/webapp/TargetedMS/js/QCPlotHelperWrapper.js @@ -215,6 +215,11 @@ Ext4.define("LABKEY.targetedms.QCPlotHelperWrapper", { } this.setPlotBrushingDisplayStyle(); + + if (this.hasPeptideGroupTree()) { + this.renderCombinedTreeLegend(id, legendMargin); + } + return true; }, @@ -295,6 +300,12 @@ Ext4.define("LABKEY.targetedms.QCPlotHelperWrapper", { this.fragmentPlotData[fragment] = this.getInitFragmentPlotData(fragment, dataType, mz, color); } + // Store peptide group info (protein / molecule list) for the combined plot tree legend + if (this.fragmentPlotData[fragment].peptideGroupId == null && plotDataRow['PeptideGroupId'] != null) { + this.fragmentPlotData[fragment].peptideGroupId = plotDataRow['PeptideGroupId']; + this.fragmentPlotData[fragment].peptideGroupLabel = plotDataRow['PeptideGroupLabel']; + } + var metricId = row['MetricId']; const metricProp = metricProps[metricId]; diff --git a/webapp/TargetedMS/js/QCTrendPlotPanel.js b/webapp/TargetedMS/js/QCTrendPlotPanel.js index c06bf3837..65f74f7e6 100644 --- a/webapp/TargetedMS/js/QCTrendPlotPanel.js +++ b/webapp/TargetedMS/js/QCTrendPlotPanel.js @@ -148,6 +148,17 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { } initValues[key] = annotations; } + else if (key === 'hiddenSeries' && value) { + try { + var hiddenArr = JSON.parse(value); + var hiddenMap = {}; + if (Array.isArray(hiddenArr)) { + hiddenArr.forEach(function(f) { hiddenMap[f] = true; }); + } + initValues['hiddenPrecursorSeries'] = hiddenMap; + } + catch (e) { /* ignore malformed stored value */ } + } else { initValues[key] = value; } @@ -1650,6 +1661,8 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { } this.hiddenPrecursorSeries[fragment] = !this.hiddenPrecursorSeries[fragment]; this.applySeriesVisibility(); + this.havePlotOptionsChanged = true; + this.persistSelectedFormOptions(); }, applySeriesVisibility: function() { @@ -1669,6 +1682,8 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { var opacity = hidden[d.hoverText || d.name.split('|')[0]] ? 0.3 : 1; d3.select(this).attr('fill-opacity', opacity).attr('stroke-opacity', opacity); }); + + this.updateTreeLegendState(); }, attachCombinedLegendClickHandlers: function() { @@ -1685,6 +1700,143 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { this.applySeriesVisibility(); }, + // Build an ordered list of protein/molecule-list groups from fragmentPlotData for the combined plot. + buildPeptideGroups: function() { + let groups = {}; + for (var i = 0; i < this.precursors.length; i++) { + let fragment = this.precursors[i]; + let info = this.fragmentPlotData[fragment]; + if (!info || info.peptideGroupId == null) continue; + let gid = info.peptideGroupId; + if (!groups[gid]) { + groups[gid] = { id: gid, label: info.peptideGroupLabel || '', fragments: [] }; + } + groups[gid].fragments.push(fragment); + } + let groupArray = Object.values(groups); + groupArray.sort(function(a, b) { return a.id - b.id; }); + return groupArray; + }, + + hasPeptideGroupTree: function() { + return !!this.peptideGroups && this.peptideGroups.length > 1; + }, + + renderCombinedTreeLegend: function(firstPlotId, legendMargin) { + let existing = document.getElementById('qc-combined-tree-legend'); + if (existing) existing.parentNode.removeChild(existing); + + if (!this.hasPeptideGroupTree()) return; + + let plotEl = document.getElementById(firstPlotId); + if (!plotEl) return; + + plotEl.style.position = 'relative'; + + let treeDiv = document.createElement('div'); + treeDiv.id = 'qc-combined-tree-legend'; + treeDiv.className = 'qc-combined-tree-legend'; + treeDiv.style.cssText = [ + 'position: absolute', + 'right: 0', + 'top: 65px', + 'width: ' + legendMargin + 'px', + 'overflow-y: auto', + 'max-height: 430px', + 'font-size: 11px', + 'font-family: Roboto, arial, helvetica, sans-serif', + 'padding: 0 4px', + 'box-sizing: border-box' + ].join('; '); + + treeDiv.innerHTML = this.buildTreeLegendHTML(); + plotEl.appendChild(treeDiv); + this.attachTreeLegendHandlers(treeDiv); + }, + + buildTreeLegendHTML: function() { + let hidden = this.hiddenPrecursorSeries || {}; + let html = ''; + for (let g = 0; g < this.peptideGroups.length; g++) { + let group = this.peptideGroups[g]; + let allHidden = group.fragments.every(function(f) { return !!hidden[f]; }); + html += '
'; + html += ''; + html += '
'; + for (let p = 0; p < group.fragments.length; p++) { + let fragment = group.fragments[p]; + let info = this.fragmentPlotData[fragment]; + if (!info) continue; + let text = this.legendHelper.getLegendItemText(info); + let color = info.color || '#000000'; + let opacity = hidden[fragment] ? '0.3' : '1'; + html += '
'; + html += ''; + html += '' + Ext4.util.Format.htmlEncode(text) + ''; + html += '
'; + } + html += '
'; + } + return html; + }, + + attachTreeLegendHandlers: function(treeDiv) { + let me = this; + let hidden = this.hiddenPrecursorSeries || {}; + + treeDiv.querySelectorAll('.qc-tree-precursor').forEach(function(el) { + el.addEventListener('click', function() { + me.toggleCombinedSeriesVisibility(el.getAttribute('data-fragment')); + }); + }); + + treeDiv.querySelectorAll('.qc-tree-group-check').forEach(function(checkbox) { + let groupIdx = parseInt(checkbox.getAttribute('data-group-idx')); + let group = me.peptideGroups[groupIdx]; + let someHidden = group.fragments.some(function(f) { return !!hidden[f]; }); + let allHidden = group.fragments.every(function(f) { return !!hidden[f]; }); + checkbox.indeterminate = someHidden && !allHidden; + + checkbox.addEventListener('change', function() { + if (!me.hiddenPrecursorSeries) me.hiddenPrecursorSeries = {}; + let shouldHide = !checkbox.checked; + group.fragments.forEach(function(f) { + if (shouldHide) { + me.hiddenPrecursorSeries[f] = true; + } else { + delete me.hiddenPrecursorSeries[f]; + } + }); + me.applySeriesVisibility(); + }); + }); + }, + + updateTreeLegendState: function() { + let treeDiv = document.getElementById('qc-combined-tree-legend'); + if (!treeDiv || !this.peptideGroups) return; + + let hidden = this.hiddenPrecursorSeries || {}; + let me = this; + + treeDiv.querySelectorAll('.qc-tree-precursor').forEach(function(el) { + el.style.opacity = hidden[el.getAttribute('data-fragment')] ? '0.3' : '1'; + }); + + treeDiv.querySelectorAll('.qc-tree-group-check').forEach(function(checkbox) { + let groupIdx = parseInt(checkbox.getAttribute('data-group-idx')); + let group = me.peptideGroups[groupIdx]; + let allHidden = group.fragments.every(function(f) { return !!hidden[f]; }); + let someHidden = group.fragments.some(function(f) { return !!hidden[f]; }); + checkbox.checked = !allHidden; + checkbox.indeterminate = someHidden && !allHidden; + }); + }, + plotBrushStartEvent : function(plot) { this.clearPlotBrush(plot); }, @@ -2701,6 +2853,10 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { } }); + var hiddenSeriesArr = Object.keys(this.hiddenPrecursorSeries || {}).filter(function(k) { + return !!this.hiddenPrecursorSeries[k]; + }, this); + var props = { metric: this.metric, metric2: this.metric2, @@ -2712,7 +2868,8 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { dateRangeOffset: this.dateRangeOffset, selectedAnnotations: annotationsProp, showExcludedPrecursors: this.showExcludedPrecursors, - trailingRuns: this.trailingRuns + trailingRuns: this.trailingRuns, + hiddenSeries: JSON.stringify(hiddenSeriesArr) }; // set start and end date to null unless we are From d36187386989d4e7476cb6162c16e11d36afb9cc Mon Sep 17 00:00:00 2001 From: ankurjuneja Date: Wed, 15 Apr 2026 09:33:30 -0700 Subject: [PATCH 4/5] persist the top level peptide checkbox state --- webapp/TargetedMS/js/QCTrendPlotPanel.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/webapp/TargetedMS/js/QCTrendPlotPanel.js b/webapp/TargetedMS/js/QCTrendPlotPanel.js index 65f74f7e6..d7fd05e30 100644 --- a/webapp/TargetedMS/js/QCTrendPlotPanel.js +++ b/webapp/TargetedMS/js/QCTrendPlotPanel.js @@ -1812,6 +1812,8 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { } }); me.applySeriesVisibility(); + me.havePlotOptionsChanged = true; + me.persistSelectedFormOptions(); }); }); }, From bcca7293b77a6b184bb0e966d066154dc50900ee Mon Sep 17 00:00:00 2001 From: ankurjuneja Date: Wed, 15 Apr 2026 09:40:52 -0700 Subject: [PATCH 5/5] update comment --- webapp/TargetedMS/css/qcTrendPlotReport.css | 1 - webapp/TargetedMS/js/QCTrendPlotPanel.js | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/webapp/TargetedMS/css/qcTrendPlotReport.css b/webapp/TargetedMS/css/qcTrendPlotReport.css index 70b1bd527..b4434fed4 100644 --- a/webapp/TargetedMS/css/qcTrendPlotReport.css +++ b/webapp/TargetedMS/css/qcTrendPlotReport.css @@ -104,7 +104,6 @@ font-weight: normal; } -/* Combined plot tree legend (shown when document has multiple proteins / molecule lists) */ .qc-combined-tree-legend { background: white; line-height: 1.4; diff --git a/webapp/TargetedMS/js/QCTrendPlotPanel.js b/webapp/TargetedMS/js/QCTrendPlotPanel.js index d7fd05e30..a048cdf4b 100644 --- a/webapp/TargetedMS/js/QCTrendPlotPanel.js +++ b/webapp/TargetedMS/js/QCTrendPlotPanel.js @@ -77,7 +77,7 @@ Ext4.define('LABKEY.targetedms.QCTrendPlotPanel', { enableBrushing: false, havePlotOptionsChanged: false, selectedAnnotations: {}, - hiddenPrecursorSeries: null, // Plain object mapping fragment label -> true when hidden in the combined plot + hiddenPrecursorSeries: null, runs: null, trailingRuns: null, minWidth: 1250, // Keep in sync with the width defined in qcTrendPlot.jsp